├── .gitattributes ├── .gitignore ├── .travis.yml ├── README.md ├── WhalesFargo.sln ├── config.json └── src ├── App.config ├── DiscordBot.cs ├── Externals ├── Discord.ico ├── ffmpeg.exe ├── ffplay.exe ├── ffprobe.exe ├── libopus.dll ├── libsodium.dll ├── opus.dll └── youtube-dl.exe ├── Helpers ├── AudioDownloader.cs ├── AudioFile.cs ├── AudioPlayer.cs ├── Config.cs ├── Strings.Designer.cs ├── Strings.resx └── Tools.cs ├── Modules ├── AdminModule.cs ├── AudioModule.cs ├── ChatModule.cs ├── CustomModule.cs └── HelpModule.cs ├── Program.cs ├── Properties ├── AssemblyInfo.cs └── app.manifest ├── Services ├── AdminService.cs ├── AudioService.cs ├── ChatService.cs └── CustomService.cs ├── UI ├── Window.Designer.cs ├── Window.cs └── Window.resx ├── WhalesFargo.csproj └── packages.config /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc 262 | 263 | # Settings 264 | **.settings 265 | **.Designer.cs 266 | src/BotToken.txt 267 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Simple Build Testing 2 | language: csharp 3 | solution: WhalesFargo.sln 4 | install: 5 | - nuget restore WhalesFargo.sln 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discord-Bot 2 | A Discord C#/.NET Bot 3 | 4 | This Discord bot is a chat and voice bot coded in C# with [Discord.Net](https://github.com/discord-net/Discord.Net) 5 | 6 | This bot is built to be run on any .NET Framework compatible machine. 7 | It is meant for single server usage due to the heavy amount of resources the voice part consumes. 8 | 9 | _This bot currently is still in development, further changes to come._ 10 | 11 | ## To Install: 12 | 13 | To install do the following: 14 | * Clone our repo. 15 | * Compile with Visual Studio, and it should be ready to go. 16 | * Modify [*config.json*](config.json) with your bot token and other settings. 17 | * Move this file to the same folder as your executable. 18 | 19 | ## Resources 20 | 21 | - [Features](https://github.com/domnguyen/Discord-Bot/wiki/Features) 22 | -------------------------------------------------------------------------------- /WhalesFargo.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27004.2006 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WhalesFargo", "src\WhalesFargo.csproj", "{8CC867BF-9384-41B8-B7DC-17B82E790DC1}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {8CC867BF-9384-41B8-B7DC-17B82E790DC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {8CC867BF-9384-41B8-B7DC-17B82E790DC1}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {8CC867BF-9384-41B8-B7DC-17B82E790DC1}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {8CC867BF-9384-41B8-B7DC-17B82E790DC1}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {D1D1D540-7939-473D-B29D-66BCAA043919} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | {"ApiKey":"your-api-key-here","DiscordToken":"your-discord-token-here","Prefix":'!',"DownloadPath":"tmp"} -------------------------------------------------------------------------------- /src/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/DiscordBot.cs: -------------------------------------------------------------------------------- 1 | #define DEBUG_VERBOSE // Use this to print out all log messages to console. Comment out to disable. 2 | 3 | using Discord; 4 | using Discord.Commands; 5 | using Discord.WebSocket; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using System; 8 | using System.Reflection; 9 | using System.Threading.Tasks; 10 | using WhalesFargo.Helpers; 11 | using WhalesFargo.Services; 12 | 13 | namespace WhalesFargo 14 | { 15 | /** 16 | * DiscordBot 17 | * Main program to run the Discord Bot. 18 | * 19 | */ 20 | public class DiscordBot 21 | { 22 | // Static variables. 23 | public static string ConnectionStatus = Strings.Disconnected; 24 | 25 | // Private variables. 26 | private DiscordSocketClient m_Client; // Discord client. 27 | private CommandService m_Commands; // Command service to link modules. 28 | private IServiceProvider m_Services; // Service provider to add services to these modules. 29 | 30 | private string m_ConfigFile = "config.json";// Configuration filename. 31 | private bool m_RetryConnection = true; // Flag for retrying connection, for the first connection. 32 | private const int m_RetryInterval = 1000; // Interval in milliseconds, for each connection attempt. 33 | private bool m_Running = false; // Flag for checking if it's running. 34 | private const int m_RunningInterval = 1000; // Interval in milliseconds to check if running. 35 | private bool m_DesktopNotifications = true; // Flag for desktop notifications in minimized mode. 36 | 37 | // Sets the connection status. 38 | private void SetConnectionStatus(string s, Exception arg = null) 39 | { 40 | ConnectionStatus = s; 41 | if (arg != null) Console.WriteLine(arg); 42 | if (Program.UI != null) { Program.UI.SetConnectionStatus(s); } 43 | } 44 | 45 | // This function is called, when the client is fully connected. 46 | private Task Connected() 47 | { 48 | SetConnectionStatus("Connected"); 49 | return Task.CompletedTask; 50 | } 51 | 52 | // This function is called, when the client suddenly disconnects. 53 | private Task Disconnected(Exception arg) 54 | { 55 | SetConnectionStatus("Disconnected", arg); 56 | return Task.CompletedTask; 57 | } 58 | 59 | // Returns if we want to send desktop notifications in the UI, from the System Tray. 60 | public bool GetDesktopNotifications() { return m_DesktopNotifications; } 61 | 62 | // Starts the async loop. 63 | public async Task RunAsync() 64 | { 65 | // Already running... 66 | if (m_Client != null) 67 | { 68 | if (m_Client.ConnectionState == ConnectionState.Connecting || 69 | m_Client.ConnectionState == ConnectionState.Connected) 70 | return; 71 | } 72 | 73 | // Read configuration 74 | Config.Instance.Read(m_ConfigFile); 75 | 76 | // Start to make the connection to the server 77 | m_Client = new DiscordSocketClient(); 78 | m_Commands = new CommandService(); // Start the command service to add all our commands. See 'InstallCommands' 79 | m_Services = InstallServices(); // We install services by adding it to a service collection. 80 | m_RetryConnection = true; // Always set reconnect to true. Set this to false when we cancel the connection. 81 | m_Running = false; // Explicit. 82 | 83 | // The bot will automatically reconnect once the initial connection is established. 84 | // To keep trying, keep it in a loop. 85 | while (true) 86 | { 87 | try // Attempt to connect. 88 | { 89 | // Set the connecting status. 90 | SetConnectionStatus("Connecting"); 91 | 92 | // Login using the bot token. 93 | await m_Client.LoginAsync(TokenType.Bot, Config.Instance.DiscordToken); 94 | 95 | // Startup the client. 96 | await m_Client.StartAsync(); 97 | 98 | // Install commands once the client has logged in. 99 | await InstallCommands(); 100 | 101 | // Successfully connected and running. 102 | m_Running = true; 103 | 104 | break; 105 | } 106 | catch 107 | { 108 | await Log(new LogMessage(LogSeverity.Error, "RunAsync", "Failed to connect.")); 109 | if (m_RetryConnection == false) 110 | { 111 | SetConnectionStatus("Disconnected"); 112 | return; 113 | } 114 | await Task.Delay(m_RetryInterval); // Make sure we don't reconnect too fast. 115 | } 116 | } 117 | 118 | // Stays in this loop while running. 119 | while (m_Running) { await Task.Delay(m_RunningInterval); } 120 | 121 | // Doesn't end the program until the whole thing is done. 122 | if (m_Client.ConnectionState == ConnectionState.Connecting || 123 | m_Client.ConnectionState == ConnectionState.Connected) 124 | { 125 | try { m_Client.StopAsync().Wait(); } 126 | catch { } 127 | } 128 | } 129 | 130 | // In the connection loop, cancels the request. 131 | public async Task CancelAsync() 132 | { 133 | m_RetryConnection = false; 134 | await Task.Delay(0); 135 | } 136 | 137 | // If connected, disconnect from the server. 138 | public async Task StopAsync() 139 | { 140 | if (m_Running) m_Running = false; 141 | await Task.Delay(0); 142 | } 143 | 144 | // This is where you install all necessary services for our bot. 145 | // TODO: Make sure to add any additional services you want here!! 146 | // In those services, if you have any commands, it will automatically 147 | // discovered in 'InstallCommands' 148 | private IServiceProvider InstallServices() 149 | { 150 | ServiceCollection services = new ServiceCollection(); 151 | 152 | // Add all additional services here. 153 | services.AddSingleton(); // AdminModule : AdminService 154 | services.AddSingleton(); // AudioModule : AudioService 155 | services.AddSingleton(); // ChatModule : ChatService 156 | 157 | // Return the service provider. 158 | return services.BuildServiceProvider(); 159 | } 160 | 161 | // This is where you install all possible commands for the Discord Client. 162 | // Essentially, it will take the Messages Received and send it into our Handler 163 | // TODO: Add any necessary functions to receive or handle Discord Socket Events. 164 | private async Task InstallCommands() 165 | { 166 | // Before we install commands, we should check if everything was set up properly. Check if logged in. 167 | if (m_Client.LoginState != LoginState.LoggedIn) return; 168 | 169 | // Hook the MessageReceived Event into our Command Handler 170 | m_Client.MessageReceived += MessageReceived; 171 | 172 | // Add tasks to send Messages, and userJoined to appropriate places 173 | m_Client.Ready += Ready; 174 | m_Client.UserJoined += UserJoined; 175 | m_Client.UserLeft += UserLeft; 176 | m_Client.Connected += Connected; 177 | m_Client.Disconnected += Disconnected; 178 | m_Client.Log += Log; 179 | 180 | // Discover all of the commands in this assembly and load them. 181 | await m_Commands.AddModulesAsync(Assembly.GetEntryAssembly(), m_Services); 182 | } 183 | 184 | // Handles commands with prefix char and mention prefix. 185 | // Others get handled differently. 186 | private async Task MessageReceived(SocketMessage messageParam) 187 | { 188 | // Don't process the command if it was a System Message 189 | var message = messageParam as SocketUserMessage; 190 | if (message == null) return; 191 | 192 | // Create a number to track where the prefix ends and the command begins 193 | int argPos = 0; 194 | 195 | // Determine if the message is a command, based on if it starts with the prefix char or a mention prefix 196 | if (!(message.HasCharPrefix(Config.Instance.Prefix, ref argPos) || message.HasMentionPrefix(m_Client.CurrentUser, ref argPos))) 197 | { 198 | // If it isn't a command, decide what to do with it here. 199 | // TODO: Add any special handlers here. 200 | return; 201 | } 202 | 203 | // Create a Command Context 204 | var context = new CommandContext(m_Client, message); 205 | 206 | // Execute the command. (result does not indicate a return value, 207 | // rather an object stating if the command executed successfully) 208 | var result = await m_Commands.ExecuteAsync(context, argPos, m_Services); 209 | if (!result.IsSuccess) // If failed, write error to chat. 210 | await context.Channel.SendMessageAsync(result.ErrorReason); 211 | } 212 | 213 | // This sets the bots status as default. Can easily be changed. 214 | private async Task Ready() 215 | { 216 | await m_Client.SetGameAsync($"Type {Config.Instance.Prefix}help for help!"); 217 | } 218 | 219 | // This function is called once a user joins the server. 220 | private async Task UserJoined(SocketGuildUser user) 221 | { 222 | var channel = user.Guild.DefaultChannel; // You can add references to any channel you wish 223 | await channel.SendMessageAsync("Welcome to the Discord server" + user.Mention + "! Feel free to ask around if you need help!"); 224 | } 225 | 226 | // This function is called once a user joins the server. 227 | private async Task UserLeft(SocketGuildUser user) 228 | { 229 | var channel = user.Guild.DefaultChannel; // You can add references to any channel you wish 230 | await channel.SendMessageAsync(user.Mention + " has left the Discord server."); 231 | } 232 | 233 | // This function is used for any client logging. 234 | private Task Log(LogMessage msg) 235 | { 236 | Console.WriteLine(msg.ToString()); 237 | if (Program.UI != null) Program.UI.SetConsoleText(msg.ToString()); 238 | return Task.CompletedTask; 239 | } 240 | 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /src/Externals/Discord.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domnguyen/Discord-Bot/c0027d5798b79840909f29ab7494f6b8970dff71/src/Externals/Discord.ico -------------------------------------------------------------------------------- /src/Externals/ffmpeg.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domnguyen/Discord-Bot/c0027d5798b79840909f29ab7494f6b8970dff71/src/Externals/ffmpeg.exe -------------------------------------------------------------------------------- /src/Externals/ffplay.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domnguyen/Discord-Bot/c0027d5798b79840909f29ab7494f6b8970dff71/src/Externals/ffplay.exe -------------------------------------------------------------------------------- /src/Externals/ffprobe.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domnguyen/Discord-Bot/c0027d5798b79840909f29ab7494f6b8970dff71/src/Externals/ffprobe.exe -------------------------------------------------------------------------------- /src/Externals/libopus.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domnguyen/Discord-Bot/c0027d5798b79840909f29ab7494f6b8970dff71/src/Externals/libopus.dll -------------------------------------------------------------------------------- /src/Externals/libsodium.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domnguyen/Discord-Bot/c0027d5798b79840909f29ab7494f6b8970dff71/src/Externals/libsodium.dll -------------------------------------------------------------------------------- /src/Externals/opus.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domnguyen/Discord-Bot/c0027d5798b79840909f29ab7494f6b8970dff71/src/Externals/opus.dll -------------------------------------------------------------------------------- /src/Externals/youtube-dl.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/domnguyen/Discord-Bot/c0027d5798b79840909f29ab7494f6b8970dff71/src/Externals/youtube-dl.exe -------------------------------------------------------------------------------- /src/Helpers/AudioDownloader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace WhalesFargo.Helpers 9 | { 10 | /** 11 | * AudioDownloader 12 | * Helper class to download files in the background. 13 | * This can be used to optimize network audio sources. 14 | * This is also the only class that's using youtube-dl, 15 | * which will be the primary executable for downloading audio. 16 | */ 17 | public class AudioDownloader 18 | { 19 | // Concurrent Library to keep track of the current downloads order. 20 | private readonly ConcurrentQueue m_DownloadQueue = new ConcurrentQueue(); 21 | 22 | // Private variables. 23 | private string m_DownloadPath = "tmp"; // Default folder path. This is relative to the running directory of the bot. 24 | private bool m_IsRunning = false; // Flag to check if it's in the middle of downloading already. 25 | private string m_CurrentlyDownloading = ""; // Currently downloading file. 26 | private bool m_AllowDuplicates = true; // Flag for downloading duplicate items. 27 | 28 | // Sets the current downloading folder. 29 | public void SetDownloadPath(string path) 30 | { 31 | // Update download path 32 | if (path != null) 33 | m_DownloadPath = path; 34 | 35 | // Create the directory if it does not exist 36 | if (!Directory.Exists(m_DownloadPath)) 37 | Directory.CreateDirectory(m_DownloadPath); 38 | } 39 | 40 | // Returns the status of the downloader. 41 | public bool IsRunning() { return m_IsRunning; } 42 | 43 | // Returns the current download status. 44 | public string CurrentlyDownloading() { return m_CurrentlyDownloading; } 45 | 46 | // Returns a string with downloaded song names. 47 | public string[] GetAllItems() 48 | { 49 | // Check the files in the directory. 50 | string[] itemEntries = Directory.GetFiles(m_DownloadPath); 51 | int itemCount = itemEntries.Length; 52 | if (itemCount == 0) return new string[] { "There are currently no items downloaded." }; 53 | return itemEntries; 54 | } 55 | 56 | // Returns a path to the downloaded item, if already downloaded. 57 | public string GetItem(string item) 58 | { 59 | // If it's been downloaded and isn't currently downloading, we can return it. 60 | try 61 | { 62 | if (File.Exists($"{m_DownloadPath}\\{item}") && !m_CurrentlyDownloading.Equals(item)) 63 | return $"{m_DownloadPath}\\{item}"; 64 | } catch { } 65 | // Check by filename without .mp3. 66 | try 67 | { 68 | if (File.Exists($"{m_DownloadPath}\\{item}.mp3") && !m_CurrentlyDownloading.Equals(item)) 69 | return $"{m_DownloadPath}\\{item}.mp3"; 70 | } catch { } 71 | 72 | // Else we return blank. This means the item doesn't exist in our library. 73 | return null; 74 | } 75 | 76 | // Returns a path to the downloaded item, if already downloaded. 77 | public string GetItem(int index) 78 | { 79 | // Check the files in the directory. 80 | string[] itemEntries = Directory.GetFiles(m_DownloadPath); 81 | 82 | // Return by index. 83 | if (index < 0 || index >= itemEntries.Length) return null; 84 | return itemEntries[index].Split(Path.DirectorySeparatorChar).Last(); 85 | } 86 | 87 | // Returns the proper filename by searching the path for an existing file. 88 | // We use the song title we're searching for, without the .mp3. 89 | private string GetDuplicateItem(string item) 90 | { 91 | string filename = null; 92 | int count = 0; 93 | 94 | filename = Path.Combine(m_DownloadPath, item + ".mp3"); 95 | 96 | while (File.Exists(filename)) 97 | { 98 | filename = Path.Combine(m_DownloadPath, item + "_" + (count++) + ".mp3"); 99 | } 100 | 101 | return filename; 102 | } 103 | 104 | // Remove any duplicates created by the downloader. 105 | public void RemoveDuplicateItems() 106 | { 107 | ConcurrentDictionary duplicates = new ConcurrentDictionary(); 108 | 109 | // Check the files in the directory. 110 | string[] itemEntries = Directory.GetFiles(m_DownloadPath); 111 | foreach (string item in itemEntries) 112 | { 113 | string filename = Path.GetFileNameWithoutExtension(item); 114 | 115 | // If it's a duplicate, get it's base name. 116 | var isDuplicate = int.TryParse(filename.Split('_').Last(), out int n); 117 | if (isDuplicate) filename = filename.Split(new char[] { '_' }, 2)[0]; 118 | 119 | // Get the current count, then update the count. 120 | duplicates.TryRemove(filename, out int count); 121 | duplicates.TryAdd(filename, ++count); 122 | 123 | try { if (count >= 2) File.Delete(item); } 124 | catch { Console.WriteLine("Problem while deleting duplicates."); } 125 | } 126 | } 127 | 128 | // Gets the next song in the queue for download. 129 | private AudioFile Pop() 130 | { 131 | m_DownloadQueue.TryDequeue(out AudioFile nextSong); 132 | return nextSong; 133 | } 134 | 135 | // Adds a song to the queue for download. 136 | public void Push(AudioFile song) { m_DownloadQueue.Enqueue(song); } // Only add if there's no errors. 137 | 138 | // Starts the download loop and downloads from the front of the queue. 139 | public async Task StartDownloadAsync() 140 | { 141 | if (m_IsRunning) return; // Download is already running, stop to avoid conflicts/race conditions. 142 | 143 | // Loop for downloading. 144 | m_IsRunning = true; 145 | while (m_DownloadQueue.Count > 0) 146 | { 147 | if (!m_IsRunning) return; // Stop downloading. 148 | await DownloadAsync(Pop()); 149 | } 150 | m_IsRunning = false; 151 | } 152 | 153 | // Downloads the file in the background and sets downloaded to true when done. 154 | // This can be used to optimize network audio sources. 155 | private async Task DownloadAsync(AudioFile song) 156 | { 157 | // First we check if it's a network file that needs to be downloaded. 158 | if (!song.IsNetwork) return; 159 | 160 | // Then we check if the file already exists. 161 | string filename = GetItem(song.Title + ".mp3"); 162 | if (filename != null) // We get the full path. 163 | { 164 | if (m_AllowDuplicates) filename = GetDuplicateItem(song.Title); 165 | else return; 166 | } 167 | else // This is our first time seeing it. 168 | { 169 | filename = m_DownloadPath + "\\" + song.Title + ".mp3"; 170 | } 171 | 172 | { // Start downloading. 173 | // Set it as our currently downloading item. 174 | m_CurrentlyDownloading = filename; 175 | Console.WriteLine("Currently downloading : " + song.Title); 176 | 177 | // youtube-dl.exe 178 | Process youtubedl; 179 | 180 | try 181 | { 182 | // Download Video. This replaces the format with the extension .mp3 in the end. 183 | ProcessStartInfo youtubedlFile = new ProcessStartInfo() 184 | { 185 | FileName = "youtube-dl", 186 | Arguments = $"-x --audio-format mp3 -o \"{filename.Replace(".mp3", ".%(ext)s")}\" {song.FileName}", 187 | CreateNoWindow = true, 188 | RedirectStandardOutput = true, 189 | UseShellExecute = false 190 | }; 191 | youtubedl = Process.Start(youtubedlFile); 192 | youtubedl.WaitForExit(); 193 | } 194 | catch 195 | { 196 | // Error while downloading. Remove from folder if exists. 197 | Console.WriteLine("Error while downloading " + song.Title); 198 | if (GetItem(filename) != null) File.Delete(filename); 199 | } 200 | 201 | // Update the filename with the local directory, set it to local and downloaded to true. 202 | // Remember, the title is already set. 203 | song.FileName = filename; 204 | song.IsNetwork = false; // Network is now false. 205 | song.IsDownloaded = true; 206 | m_CurrentlyDownloading = ""; // Reset our currently downloading item. 207 | } 208 | 209 | await Task.Delay(0); 210 | } 211 | 212 | // Stops the download loop. 213 | public void StopDownload() 214 | { 215 | m_IsRunning = false; 216 | } 217 | 218 | // Verifies that the path is a network path and not a local path. Checks here before extracting. 219 | // TODO: Add more arguments here, but we'll just check based on http and assume a network link. 220 | public bool? VerifyNetworkPath(string path) 221 | { 222 | if (path == null) return null; 223 | return path.StartsWith("http"); 224 | } 225 | 226 | // Extracts data from the current path, by finding it locally or on the network. 227 | // Puts all the information into an AudioFile and returns it. 228 | // 229 | // Filename - source by local filename or from network link. 230 | // Title - name of the song. 231 | // IsNetwork - If it's local or network. 232 | public async Task GetAudioFileInfo(string path) 233 | { 234 | if (path == null) return null; 235 | Console.WriteLine("Extracting Meta Data for : " + path); 236 | 237 | // Verify if it's a network path or not. 238 | bool? verifyNetwork = VerifyNetworkPath(path); 239 | if (verifyNetwork == null) 240 | { 241 | Console.WriteLine("Path invalid."); 242 | return null; 243 | } 244 | 245 | // Construct audio file. 246 | AudioFile StreamData = new AudioFile(); 247 | 248 | // Local file. 249 | if (verifyNetwork == false) 250 | { 251 | try 252 | { 253 | // Check if we have it in our downloaded directory. 254 | string downloaded = GetItem(path); 255 | if (downloaded != null) path = downloaded; 256 | 257 | // If it's downloaded, it'll exist, but if it still doesn't exist, return. 258 | if (!File.Exists(path)) 259 | { 260 | Console.WriteLine("File does not exist."); 261 | throw new NullReferenceException(); 262 | } 263 | 264 | // Set the file name. 265 | StreamData.FileName = path; 266 | 267 | // Extract corresponding data. 268 | StreamData.Title = path.Split(Path.DirectorySeparatorChar).Last(); 269 | if (StreamData.Title.CompareTo("") == 0) StreamData.Title = path; 270 | 271 | // Set other properties as follows. 272 | StreamData.IsNetwork = (bool)verifyNetwork; 273 | } 274 | catch 275 | { 276 | Console.WriteLine("Failed to get local file information!"); 277 | return null; 278 | } 279 | } 280 | // Network file. 281 | else if (verifyNetwork == true) 282 | { 283 | // youtube-dl.exe 284 | Process youtubedl; 285 | 286 | try 287 | { 288 | // Get Video Title 289 | ProcessStartInfo youtubedlMetaData = new ProcessStartInfo() 290 | { 291 | FileName = "youtube-dl", 292 | Arguments = $"-s -e {path}",// Add more flags for more options. 293 | CreateNoWindow = true, 294 | RedirectStandardOutput = true, 295 | UseShellExecute = false 296 | }; 297 | youtubedl = Process.Start(youtubedlMetaData); 298 | youtubedl.WaitForExit(); 299 | 300 | // Read the output of the simulation 301 | string[] output = youtubedl.StandardOutput.ReadToEnd().Split('\n'); 302 | 303 | // Set the file name. 304 | StreamData.FileName = path; 305 | 306 | // Extract each line printed for it's corresponding data. 307 | if (output.Length > 0) 308 | StreamData.Title = output[0]; 309 | 310 | // Set other properties as follows. 311 | StreamData.IsNetwork = (bool)verifyNetwork; 312 | } 313 | catch 314 | { 315 | Console.WriteLine("youtube-dl.exe failed to extract the data!"); 316 | return null; 317 | } 318 | } 319 | 320 | await Task.Delay(0); 321 | return StreamData; 322 | } 323 | 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /src/Helpers/AudioFile.cs: -------------------------------------------------------------------------------- 1 | namespace WhalesFargo.Helpers 2 | { 3 | /** 4 | * AudioFile 5 | * Class that holds properties from the audio file. 6 | * Add more when necessary, but the only thing we're using for it now is the title field. 7 | */ 8 | public class AudioFile 9 | { 10 | private string m_FileName; 11 | private string m_Title; 12 | private bool m_IsNetwork; 13 | private bool m_IsDownloaded; 14 | 15 | public AudioFile() 16 | { 17 | m_FileName = ""; 18 | m_Title = ""; 19 | m_IsNetwork = true; // True by default, streamed from the network 20 | m_IsDownloaded = false; 21 | } 22 | 23 | public override string ToString() 24 | { 25 | return m_Title; 26 | } 27 | 28 | public string FileName 29 | { 30 | get { return m_FileName; } 31 | set { m_FileName = value; } 32 | } 33 | 34 | public string Title 35 | { 36 | get { return m_Title; } 37 | set { m_Title = value; } 38 | } 39 | 40 | public bool IsNetwork 41 | { 42 | get { return m_IsNetwork; } 43 | set { m_IsNetwork = value; } 44 | } 45 | 46 | public bool IsDownloaded 47 | { 48 | get { return m_IsDownloaded; } 49 | set { m_IsDownloaded = value; } 50 | } 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Helpers/AudioPlayer.cs: -------------------------------------------------------------------------------- 1 | using Discord.Audio; 2 | using System; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Threading.Tasks; 6 | 7 | namespace WhalesFargo.Helpers 8 | { 9 | /** 10 | * AudioPlayer 11 | * Helper class to handle a single audio playback. 12 | */ 13 | class AudioPlayer 14 | { 15 | // Private variables. 16 | private bool m_IsRunning = false; // Boolean to wrap the audio playback method. 17 | private Process m_Process = null; // Process that runs when playing. 18 | private Stream m_Stream = null; // Stream output when playing. 19 | private bool m_IsPlaying = false; // Flag to change to play or pause the audio. 20 | private float m_Volume = 1.0f; // Volume value that's checked during playback. Reference: PlayAudioAsync. 21 | private int m_BLOCK_SIZE = 3840; // Custom block size for playback, in bytes. 22 | 23 | // Creates a local stream using the file path specified and ffmpeg to stream it directly. 24 | // The format Discord takes is 16-bit 48000Hz PCM 25 | private Process CreateLocalStream(string path) 26 | { 27 | try 28 | { 29 | return Process.Start(new ProcessStartInfo 30 | { 31 | FileName = "ffmpeg.exe", 32 | Arguments = $"-hide_banner -loglevel panic -i \"{path}\" -ac 2 -f s16le -ar 48000 pipe:1", 33 | UseShellExecute = false, 34 | RedirectStandardOutput = true, 35 | CreateNoWindow = true 36 | }); 37 | } 38 | catch 39 | { 40 | Console.WriteLine($"Error while opening local stream : {path}"); 41 | return null; 42 | } 43 | } 44 | 45 | // Creates a network stream using youtube-dl.exe, then piping it to ffmpeg to stream it directly. 46 | // The format Discord takes is 16-bit 48000Hz PCM 47 | // TODO: Catch any errors that happen when creating PCM streams. 48 | private Process CreateNetworkStream(string path) 49 | { 50 | try 51 | { 52 | return Process.Start(new ProcessStartInfo 53 | { 54 | FileName = "cmd.exe", 55 | Arguments = $"/C youtube-dl.exe -o - {path} | ffmpeg -i pipe:0 -ac 2 -f s16le -ar 48000 pipe:1", 56 | UseShellExecute = false, 57 | RedirectStandardOutput = true, 58 | CreateNoWindow = true 59 | }); 60 | } 61 | catch 62 | { 63 | Console.WriteLine($"Error while opening network stream : {path}"); 64 | return null; 65 | } 66 | } 67 | 68 | // Async function that handles the playback of the audio. This function is technically blocking in it's for loop. 69 | // It can be broken by cancelling m_Process or when it reads to the end of the file. 70 | // At the start, m_Process, m_Stream, amd m_IsPlaying is flushed. 71 | // While it is playing, these will hold values of the current playback audio. It will depend on m_Volume for the volume. 72 | // In the end, the three are flushed again. 73 | private async Task AudioPlaybackAsync(IAudioClient client, AudioFile song) 74 | { 75 | // Set running to true. 76 | m_IsRunning = true; 77 | 78 | // Start a new process and create an output stream. Decide between network or local. 79 | m_Process = (bool)song.IsNetwork ? CreateNetworkStream(song.FileName) : CreateLocalStream(song.FileName); 80 | m_Stream = client.CreatePCMStream(AudioApplication.Music); // Consider setting custom bitrate, buffers, and packet loss props. 81 | m_IsPlaying = true; // Set this to true to start the loop properly. 82 | 83 | await Task.Delay(5000); // We should wait for ffmpeg to buffer some of the audio first. 84 | 85 | // We stream the audio in chunks. 86 | while (true) 87 | { 88 | // If the process is already over, we're finished. If something else kills this process, we stop. 89 | if (m_Process == null || m_Process.HasExited) break; 90 | 91 | // If the stream is broken, we exit. 92 | if (m_Stream == null) break; 93 | 94 | // We pause within this function while it's 'not playing'. 95 | if (!m_IsPlaying) continue; 96 | 97 | // Read the stream in chunks. 98 | int blockSize = m_BLOCK_SIZE; // Size of bytes to read per frame. 99 | byte[] buffer = new byte[blockSize]; 100 | int byteCount; 101 | byteCount = await m_Process.StandardOutput.BaseStream.ReadAsync(buffer, 0, blockSize); 102 | 103 | // If the stream cannot be read or we reach the end of the file, we exit. 104 | if (byteCount <= 0) break; 105 | 106 | try 107 | { 108 | // Write out to the stream. Relies on m_Volume to adjust bytes accordingly. 109 | await m_Stream.WriteAsync(ScaleVolumeSafeAllocateBuffers(buffer, m_Volume), 0, byteCount); 110 | } 111 | catch (Exception exception) 112 | { 113 | Console.WriteLine(exception); 114 | break; 115 | } 116 | } 117 | 118 | // Kill the process, if it's lingering. 119 | if (m_Process != null && !m_Process.HasExited) m_Process.Kill(); 120 | 121 | // Flush the stream and wait until it's fully done before continuing. 122 | if (m_Stream != null) m_Stream.FlushAsync().Wait(); 123 | 124 | // Reset values. Basically clearing out values (Flush). 125 | m_Process = null; 126 | m_Stream = null; 127 | m_IsPlaying = false; 128 | 129 | // Set running to false. 130 | m_IsRunning = false; 131 | } 132 | 133 | // Adjusts the byte array by the volume, scaling it by a factor [0.0f, 1.0f] 134 | private byte[] ScaleVolumeSafeAllocateBuffers(byte[] audioSamples, float volume) 135 | { 136 | if (audioSamples == null) return null; 137 | if (audioSamples.Length % 2 != 0) return null; 138 | if (volume < 0.0f || volume > 1.0f) return null; 139 | 140 | // Adjust the output for the volume. 141 | var output = new byte[audioSamples.Length]; 142 | try 143 | { 144 | // If it's close to full volume, we just copy it. 145 | if (Math.Abs(volume - 1f) < 0.0001f) 146 | { 147 | Buffer.BlockCopy(audioSamples, 0, output, 0, audioSamples.Length); 148 | return output; 149 | } 150 | 151 | // 16-bit precision for the multiplication 152 | int volumeFixed = (int)Math.Round(volume * 65536d); 153 | for (var i = 0; i < output.Length; i += 2) 154 | { 155 | // The cast to short is necessary to get a sign-extending conversion 156 | int sample = (short)((audioSamples[i + 1] << 8) | audioSamples[i]); 157 | int processed = (sample * volumeFixed) >> 16; 158 | 159 | output[i] = (byte)processed; 160 | output[i + 1] = (byte)(processed >> 8); 161 | } 162 | return output; 163 | } 164 | catch (Exception exception) 165 | { 166 | Console.WriteLine(exception); 167 | return null; 168 | } 169 | } 170 | 171 | // Adjusts the current volume to the value passed. This affects the current AudioPlaybackAsync. 172 | public void AdjustVolume(float volume) 173 | { 174 | // Adjust bounds 175 | if (volume < 0.0f) 176 | volume = 0.0f; 177 | else if (volume > 1.0f) 178 | volume = 1.0f; 179 | 180 | m_Volume = volume; // Update the volume 181 | } 182 | 183 | // Returns if AudioPlaybackAsync is currently running. 184 | public bool IsRunning() { return m_IsRunning; } 185 | 186 | // Returns if the process is in the middle of AudioPlaybackAsync. 187 | public bool IsPlaying() { return ((m_Process != null) && m_IsPlaying); } 188 | 189 | // Starts the audioplayer playback for the specific song. 190 | // If something else is already playing, we stop it before putting this into the loop. 191 | public async Task Play(IAudioClient client, AudioFile song) 192 | { 193 | // Stop the current song. We wait until it's done to play the next song. 194 | if (m_IsRunning) Stop(); 195 | while (m_IsRunning) await Task.Delay(1000); 196 | 197 | // Start playback. 198 | await AudioPlaybackAsync(client, song); 199 | } 200 | 201 | // Pauses the stream if it's playing. This affects the current AudioPlaybackAsync. 202 | public void Pause() { m_IsPlaying = false; } 203 | 204 | // Resumes the stream if it's playing, but paused. This affects the current AudioPlaybackAsync. 205 | public void Resume() { m_IsPlaying = true; } 206 | 207 | // Stops the stream if it's playing. This affects the current AudioPlaybackAsync. 208 | public void Stop() { if (m_Process != null) m_Process.Kill(); } // This basically stops the current loop by exiting the process. 209 | 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/Helpers/Config.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using Newtonsoft.Json; 3 | 4 | namespace WhalesFargo.Helpers 5 | { 6 | /** 7 | * Config 8 | * Singleton class that handles basic configuration parameters for the app. 9 | * This class is not thread-safe. 10 | * TODO: Convert to thread-safe solution 11 | */ 12 | public sealed class Config 13 | { 14 | private static Config m_Instance; 15 | private static string m_FileName; 16 | 17 | private Config() { } 18 | 19 | public static Config Instance 20 | { 21 | get 22 | { 23 | if (m_Instance == null) 24 | m_Instance = new Config(); 25 | return m_Instance; 26 | } 27 | } 28 | 29 | public void Read(string filename) 30 | { 31 | if (!File.Exists(filename)) return; 32 | if (m_Instance == null) return; 33 | m_FileName = filename; 34 | JsonConvert.PopulateObject(File.ReadAllText(filename), m_Instance); 35 | } 36 | 37 | public void Write() 38 | { 39 | File.WriteAllText(m_FileName, JsonConvert.SerializeObject(m_Instance)); 40 | } 41 | 42 | [JsonProperty] 43 | public string ApiKey { get; set; } 44 | 45 | [JsonProperty] 46 | public string DiscordToken { get; set; } 47 | 48 | [JsonProperty] 49 | public char Prefix { get; set; } 50 | 51 | [JsonProperty] 52 | public string DownloadPath { get; set; } 53 | } 54 | } -------------------------------------------------------------------------------- /src/Helpers/Strings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace WhalesFargo.Helpers { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | public class Strings { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Strings() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | public static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("WhalesFargo.Helpers.Strings", typeof(Strings).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | public static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to Cancel. 65 | /// 66 | public static string CancelButton { 67 | get { 68 | return ResourceManager.GetString("CancelButton", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to Connect. 74 | /// 75 | public static string ConnectButton { 76 | get { 77 | return ResourceManager.GetString("ConnectButton", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to Connected. 83 | /// 84 | public static string Connected { 85 | get { 86 | return ResourceManager.GetString("Connected", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to Connecting. 92 | /// 93 | public static string Connecting { 94 | get { 95 | return ResourceManager.GetString("Connecting", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized string similar to Disconnect. 101 | /// 102 | public static string DisconnectButton { 103 | get { 104 | return ResourceManager.GetString("DisconnectButton", resourceCulture); 105 | } 106 | } 107 | 108 | /// 109 | /// Looks up a localized string similar to Disconnected. 110 | /// 111 | public static string Disconnected { 112 | get { 113 | return ResourceManager.GetString("Disconnected", resourceCulture); 114 | } 115 | } 116 | 117 | /// 118 | /// Looks up a localized string similar to Are you sure you want to disconnect from the current server?. 119 | /// 120 | public static string DisconnectPrompt { 121 | get { 122 | return ResourceManager.GetString("DisconnectPrompt", resourceCulture); 123 | } 124 | } 125 | 126 | /// 127 | /// Looks up a localized string similar to Disconnect Client. 128 | /// 129 | public static string DisconnectPromptTitle { 130 | get { 131 | return ResourceManager.GetString("DisconnectPromptTitle", resourceCulture); 132 | } 133 | } 134 | 135 | /// 136 | /// Looks up a localized string similar to Nothing. 137 | /// 138 | public static string NotPlaying { 139 | get { 140 | return ResourceManager.GetString("NotPlaying", resourceCulture); 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Helpers/Strings.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | Cancel 122 | 123 | 124 | Connect 125 | 126 | 127 | Connected 128 | 129 | 130 | Connecting 131 | 132 | 133 | Disconnect 134 | 135 | 136 | Disconnected 137 | 138 | 139 | Are you sure you want to disconnect from the current server? 140 | 141 | 142 | Disconnect Client 143 | 144 | 145 | Nothing 146 | 147 | -------------------------------------------------------------------------------- /src/Helpers/Tools.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | namespace WhalesFargo.Helpers 5 | { 6 | public static class Tools 7 | { 8 | // Check if a process is running or not. 9 | public static bool IsRunning(this Process process) 10 | { 11 | try { Process.GetProcessById(process.Id); } 12 | catch (InvalidOperationException) { return false; } 13 | catch (ArgumentException) { return false; } 14 | return true; 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Modules/AdminModule.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.Commands; 3 | using System.Threading.Tasks; 4 | using WhalesFargo.Services; 5 | 6 | namespace WhalesFargo.Modules 7 | { 8 | /** 9 | * AdminModule 10 | * Perform administrative level commands. 11 | */ 12 | [Name("Admin")] 13 | [Summary("Admin module to manage this discord server.")] 14 | public class AdminModule : CustomModule 15 | { 16 | // Private variables 17 | private readonly AdminService m_Service; 18 | 19 | // Dependencies are automatically injected via this constructor. 20 | // Remember to add an instance of the service. 21 | // to your IServiceCollection when you initialize your bot! 22 | public AdminModule(AdminService service) 23 | { 24 | m_Service = service; 25 | m_Service.SetParentModule(this); // Reference to this from the service. 26 | } 27 | 28 | [Command("prefix")] 29 | [Remarks("prefix [new prefix]")] 30 | [Summary("This allows admins change the command prefix.")] 31 | [RequireUserPermission(GuildPermission.Administrator)] 32 | public async Task ChangePrefix([Remainder] char prefix) 33 | { 34 | await m_Service.ChangePrefix(prefix); 35 | } 36 | 37 | [Command("mute")] 38 | [Remarks("mute [user]")] 39 | [Summary("This allows admins to mute users.")] 40 | [RequireUserPermission(GuildPermission.Administrator)] 41 | [RequireUserPermission(GuildPermission.MuteMembers)] 42 | public async Task MuteUser([Remainder] IGuildUser user) 43 | { 44 | await m_Service.MuteUser(Context.Guild, user); 45 | } 46 | 47 | [Command("unmute")] 48 | [Remarks("unmute [user]")] 49 | [Summary("This allows admins to unmute users.")] 50 | [RequireUserPermission(GuildPermission.Administrator)] 51 | [RequireUserPermission(GuildPermission.MuteMembers)] 52 | public async Task UnmuteUser([Remainder] IGuildUser user) 53 | { 54 | await m_Service.UnmuteUser(Context.Guild, user); 55 | } 56 | 57 | [Command("kick")] 58 | [Remarks("kick [user] [reason]")] 59 | [Summary("This allows admins to kick users.")] 60 | [RequireUserPermission(GuildPermission.Administrator)] 61 | [RequireUserPermission(GuildPermission.KickMembers)] 62 | public async Task KickUser(IGuildUser user, [Remainder] string reason = null) 63 | { 64 | await m_Service.KickUser(Context.Guild, user, reason); 65 | } 66 | 67 | [Command("ban")] 68 | [Remarks("ban [user] [reason]")] 69 | [Summary("This allows admins to ban users.")] 70 | [RequireUserPermission(GuildPermission.Administrator)] 71 | [RequireUserPermission(GuildPermission.BanMembers)] 72 | public async Task BanUser(IGuildUser user, [Remainder] string reason = null) 73 | { 74 | await m_Service.BanUser(Context.Guild, user, reason); 75 | } 76 | 77 | [Command("addrole")] 78 | [Remarks("addrole [user]")] 79 | [Summary("This allows admins to add specific roles to a user.")] 80 | [RequireUserPermission(GuildPermission.Administrator)] 81 | [RequireUserPermission(GuildPermission.ManageRoles)] 82 | public async Task AddRoleUser(IGuildUser user, [Remainder]string role) 83 | { 84 | await m_Service.AddRoleUser(Context.Guild, user, role); 85 | } 86 | 87 | [Command("removerole")] 88 | [Alias("removerole", "delrole")] 89 | [Remarks("delrole [user]")] 90 | [Summary("This allows admins to remove specific roles to a user.")] 91 | [RequireUserPermission(GuildPermission.Administrator)] 92 | [RequireUserPermission(GuildPermission.ManageRoles)] 93 | public async Task RemoveRoleUser(IGuildUser user, [Remainder]string role) 94 | { 95 | await m_Service.RemoveRoleUser(Context.Guild, user, role); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Modules/AudioModule.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.Commands; 3 | using System.Threading.Tasks; 4 | using WhalesFargo.Helpers; 5 | using WhalesFargo.Services; 6 | 7 | namespace WhalesFargo.Modules 8 | { 9 | /** 10 | * AudioModule 11 | * This handles all the audio commands for the bot. 12 | * 13 | * This supports playing local songs (from the machine the bot is running on) and most network 14 | * songs that youtube-dl supports. 15 | * 16 | * It also maintains it's own playlist and can be mixed between local and network sources. 17 | * 18 | * Since this is meant to run on a single server, it should only be joined in a single 19 | * voice channel at a time. We set it up to allow multiple channel connections, but we only 20 | * have a single instance of the audioplayer. If the bot exists in multiple servers, it will only 21 | * interact with the voice chat in the last server it received the commands in. 22 | * 23 | * As a module, this will interact with AudioService, using commands from Discord. 24 | */ 25 | [Name("Audio")] 26 | [Summary("Audio module to interact with voice chat. Currently, used to playback audio in a stream.")] 27 | public class AudioModule : CustomModule 28 | { 29 | // Private variables 30 | private readonly AudioService m_Service; 31 | 32 | // Dependencies are automatically injected via this constructor. 33 | // Remember to add an instance of the service. 34 | // to your IServiceCollection when you initialize your bot! 35 | public AudioModule(AudioService service) 36 | { 37 | m_Service = service; 38 | m_Service.SetParentModule(this); // Reference to this from the service. 39 | m_Service.SetDownloadPath(Config.Instance.DownloadPath); // Should only be called once. 40 | } 41 | 42 | // You *MUST* mark these commands with 'RunMode.Async' 43 | // otherwise the bot will not respond until the Task times out. 44 | // 45 | // Remember to add preconditions to your commands, 46 | // this is merely the minimal amount necessary. 47 | // 48 | // 'Avoid using long-running code in your modules wherever possible. 49 | // You should not be implementing very much logic into your modules, 50 | // instead, outsource to a service for that.' 51 | 52 | [Command("join", RunMode = RunMode.Async)] 53 | [Remarks("join")] 54 | [Summary("Joins the user's voice channel.")] 55 | public async Task JoinVoiceChannel() 56 | { 57 | if (m_Service.GetDelayAction()) return; // Stop multiple attempts to join too quickly. 58 | await m_Service.JoinAudioAsync(Context.Guild, (Context.User as IVoiceState).VoiceChannel); 59 | 60 | // Start the autoplay service if enabled, but not yet started. 61 | await m_Service.CheckAutoPlayAsync(Context.Guild, Context.Channel); 62 | } 63 | 64 | [Command("leave", RunMode = RunMode.Async)] 65 | [Remarks("leave")] 66 | [Summary("Leaves the current voice channel.")] 67 | public async Task LeaveVoiceChannel() 68 | { 69 | await m_Service.LeaveAudioAsync(Context.Guild); 70 | } 71 | 72 | [Command("play", RunMode = RunMode.Async)] 73 | [Remarks("play [url/index]")] 74 | [Summary("Plays a song by url or local path.")] 75 | public async Task PlayVoiceChannel([Remainder] string song) 76 | { 77 | // Play the audio. We check if audio is null when we attempt to play. This function is BLOCKING. 78 | await m_Service.ForcePlayAudioAsync(Context.Guild, Context.Channel, song); 79 | 80 | // Start the autoplay service if enabled, but not yet started. 81 | // Once force play is done, if auto play is enabled, we can resume the autoplay here. 82 | // We also write a counter to make sure this is the last play called, to avoid cascading auto plays. 83 | if (m_Service.GetNumPlaysCalled() == 0) await m_Service.CheckAutoPlayAsync(Context.Guild, Context.Channel); 84 | } 85 | 86 | [Command("play", RunMode = RunMode.Async)] 87 | public async Task PlayVoiceChannelByIndex(int index) 88 | { 89 | // Play a song by it's local index in the download folder. 90 | await PlayVoiceChannel(m_Service.GetLocalSong(index)); 91 | } 92 | 93 | [Command("pause", RunMode = RunMode.Async)] 94 | [Remarks("pause")] 95 | [Summary("Pauses the current song, if playing.")] 96 | public async Task PauseVoiceChannel() 97 | { 98 | m_Service.PauseAudio(); 99 | await Task.Delay(0); // Suppress async warrnings. 100 | } 101 | 102 | [Command("resume", RunMode = RunMode.Async)] 103 | [Remarks("resume")] 104 | [Summary("Pauses the current song, if paused.")] 105 | public async Task ResumeVoiceChannel() 106 | { 107 | m_Service.ResumeAudio(); 108 | await Task.Delay(0); // Suppress async warrnings. 109 | } 110 | 111 | [Command("stop", RunMode = RunMode.Async)] 112 | [Remarks("stop")] 113 | [Summary("Stops the current song, if playing or paused.")] 114 | public async Task StopVoiceChannel() 115 | { 116 | m_Service.StopAudio(); 117 | await Task.Delay(0); // Suppress async warrnings. 118 | } 119 | 120 | [Command("volume")] 121 | [Remarks("volume [num]")] 122 | [Summary("Changes the volume to [0 - 100].")] 123 | public async Task VolumeVoiceChannel(int volume) 124 | { 125 | m_Service.AdjustVolume((float)volume / 100.0f); 126 | await Task.Delay(0); // Suppress async warrnings. 127 | } 128 | 129 | [Command("add", RunMode = RunMode.Async)] 130 | [Remarks("add [url/index]")] 131 | [Summary("Adds a song by url or local path to the playlist.")] 132 | public async Task AddVoiceChannel([Remainder] string song) 133 | { 134 | // Add it to the playlist. 135 | await m_Service.PlaylistAddAsync(song); 136 | 137 | // Start the autoplay service if enabled, but not yet started. 138 | await m_Service.CheckAutoPlayAsync(Context.Guild, Context.Channel); 139 | } 140 | 141 | [Command("add", RunMode = RunMode.Async)] 142 | public async Task AddVoiceChannelByIndex(int index) 143 | { 144 | // Add a song by it's local index in the download folder. 145 | await AddVoiceChannel(m_Service.GetLocalSong(index)); 146 | } 147 | 148 | [Command("skip", RunMode = RunMode.Async)] 149 | [Alias("skip", "next")] 150 | [Remarks("skip")] 151 | [Summary("Skips the current song, if playing from the playlist.")] 152 | public async Task SkipVoiceChannel() 153 | { 154 | m_Service.PlaylistSkip(); 155 | await Task.Delay(0); 156 | } 157 | 158 | [Command("playlist", RunMode = RunMode.Async)] 159 | [Remarks("playlist")] 160 | [Summary("Shows what's currently in the playlist.")] 161 | public async Task PrintPlaylistVoiceChannel() 162 | { 163 | m_Service.PrintPlaylist(); 164 | await Task.Delay(0); 165 | } 166 | 167 | [Command("autoplay", RunMode = RunMode.Async)] 168 | [Remarks("autoplay [enable]")] 169 | [Summary("Starts the autoplay service on the current playlist.")] 170 | public async Task AutoPlayVoiceChannel(bool enable) 171 | { 172 | m_Service.SetAutoPlay(enable); 173 | 174 | // Start the autoplay service if already on, but not started. 175 | await m_Service.CheckAutoPlayAsync(Context.Guild, Context.Channel); 176 | } 177 | 178 | [Command("download", RunMode = RunMode.Async)] 179 | [Remarks("download [http]")] 180 | [Summary("Download songs into our local folder.")] 181 | public async Task DownloadSong([Remainder] string path) 182 | { 183 | await m_Service.DownloadSongAsync(path); 184 | } 185 | 186 | [Command("songs", RunMode = RunMode.Async)] 187 | [Remarks("songs [page]")] 188 | [Summary("Shows songs in our local folder in pages.")] 189 | public async Task PrintSongDirectory(int page = 0) 190 | { 191 | m_Service.PrintLocalSongs(page); 192 | await Task.Delay(0); 193 | } 194 | 195 | [Command("cleanupsongs", RunMode = RunMode.Async)] 196 | [Remarks("cleanupsongs")] 197 | [Summary("Cleans the local folder of duplicate files created by our downloader.")] 198 | public async Task CleanSongDirectory() 199 | { 200 | await m_Service.RemoveDuplicateSongsAsync(); 201 | } 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /src/Modules/ChatModule.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.Commands; 3 | using System.Threading.Tasks; 4 | using WhalesFargo.Services; 5 | 6 | namespace WhalesFargo.Modules 7 | { 8 | /** 9 | * ChatModule 10 | * Class that handles the Chat response portion of the program. 11 | * A chat module is created here with commands that interact with the ChatService. 12 | */ 13 | [Name("Chat")] 14 | [Summary("Chat module to interact with text chat.")] 15 | public class ChatModule : CustomModule 16 | { 17 | // Private variables 18 | private readonly ChatService m_Service; 19 | 20 | // Dependencies are automatically injected via this constructor. 21 | // Remember to add an instance of the service. 22 | // to your IServiceCollection when you initialize your bot! 23 | public ChatModule(ChatService service) 24 | { 25 | m_Service = service; 26 | m_Service.SetParentModule(this); // Reference to this from the service. 27 | } 28 | 29 | [Command("botStatus")] 30 | [Alias("botstatus")] 31 | [Remarks("botstatus [status]")] 32 | [Summary("Allows admins to set the bot's current game to [status]")] 33 | [RequireUserPermission(GuildPermission.ManageRoles)] 34 | public async Task SetBotStatus([Remainder] string botStatus) 35 | { 36 | m_Service.SetStatus(botStatus); 37 | await Task.Delay(0); 38 | } 39 | 40 | [Command("say")] 41 | [Alias("say")] 42 | [Remarks("say [msg]")] 43 | [Summary("The bot will respond in the same channel with the message said.")] 44 | public async Task Say([Remainder] string usr_msg = "") 45 | { 46 | m_Service.SayMessage(usr_msg); 47 | await Task.Delay(0); 48 | } 49 | 50 | [Command("Clear")] 51 | [Remarks("clear [num]")] 52 | [Summary("Allows admins to clear [num] amount of messages from current channel")] 53 | [RequireUserPermission(GuildPermission.ManageMessages)] 54 | public async Task ClearMessages([Remainder] int num = 0) 55 | { 56 | await m_Service.ClearMessagesAsync(Context.Guild, Context.Channel, Context.User, num); 57 | } 58 | } 59 | } 60 | 61 | 62 | -------------------------------------------------------------------------------- /src/Modules/CustomModule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Discord; 4 | using Discord.Commands; 5 | using Discord.WebSocket; 6 | 7 | namespace WhalesFargo.Modules 8 | { 9 | /** 10 | * CustomModule 11 | * Base class that adds overloaded and custom functions to the ModuleBase. 12 | * Shared functions like reply and set playing. 13 | * This should be paired with the CustomService to use these functions. 14 | */ 15 | public class CustomModule : ModuleBase 16 | { 17 | // Reply will allow the AudioService to reply in the correct text channel. 18 | public async Task ServiceReplyAsync(string s) 19 | { 20 | await ReplyAsync(s); 21 | } 22 | 23 | // Reply is the same as above except it can use the embed builder. 24 | public async Task ServiceReplyAsync(string title, EmbedBuilder emb) 25 | { 26 | await ReplyAsync(title, false, emb.Build()); // Text-To-Speech is off. 27 | } 28 | 29 | // Playing will allow the AudioService to set the current game. 30 | public async Task ServicePlayingAsync(string s) 31 | { 32 | try 33 | { 34 | await (Context.Client as DiscordSocketClient).SetGameAsync(s); 35 | } 36 | catch (Exception e) 37 | { 38 | Console.WriteLine(e); 39 | } 40 | } 41 | 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Modules/HelpModule.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.Commands; 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using WhalesFargo.Helpers; 7 | 8 | namespace WhalesFargo.Modules 9 | { 10 | /** 11 | * HelpModule 12 | * Class that handles the help functionality of the program. 13 | * This searches for all modules and prints out it's 'summary' tag. 14 | * If you search by module, it will print out all it's commands by it's 'summary' tag. 15 | * If you want it to show usage, use the "Remarks". 16 | */ 17 | public class HelpModule : ModuleBase 18 | { 19 | private readonly CommandService m_Commands; // Reference to command service 20 | private readonly IServiceProvider m_Provider; // Reference to service provider 21 | private readonly bool m_UseRemarks = true; // Set this to true to use Remarks instead. 22 | 23 | // This is a special type of module that doesn't need a service, since we link 24 | // it directly here. This adds the help functionality and the logic simply checks 25 | // for current tags and displays them if available. This requires that you have 26 | // both name and summary tags for each module and it's commmands that you want 27 | // to display here. 28 | public HelpModule(CommandService commands, IServiceProvider provider) 29 | { 30 | m_Commands = commands; 31 | m_Provider = provider; 32 | } 33 | 34 | [Command("help", RunMode = RunMode.Async)] 35 | [Alias("help", "h")] 36 | [Summary("Finds all the modules and prints out it's summary tag.")] 37 | public async Task HelpAsync() 38 | { 39 | // Get all the modules. 40 | var modules = m_Commands.Modules.Where(x => !string.IsNullOrWhiteSpace(x.Summary)); 41 | 42 | // Create an embed builder. 43 | var emb = new EmbedBuilder(); 44 | emb.WithTitle("Here is the list of modules."); 45 | 46 | // For each module... 47 | foreach (var module in modules) 48 | { 49 | bool success = false; 50 | foreach (var command in module.Commands) // Check if there are any commands 51 | { 52 | var result = await command.CheckPreconditionsAsync(Context, m_Provider); 53 | if (result.IsSuccess) 54 | { 55 | success = true; 56 | break; 57 | } 58 | } 59 | if (!success) 60 | continue; 61 | 62 | emb.AddField(module.Name, module.Summary); // Add to the list 63 | } 64 | 65 | if (emb.Fields.Count <= 0) // Added error checking in case we don't have summary tags yet. 66 | await ReplyAsync("Module information cannot be found, please try again later."); 67 | else 68 | await ReplyAsync("", false, emb.Build()); 69 | } 70 | 71 | [Command("help", RunMode = RunMode.Async)] // TODO: Change this once all summaries are added. 72 | [Alias("help", "h")] 73 | [Summary("Finds all the commands from a specific module and prints out it's summary tag.")] 74 | public async Task HelpAsync(string moduleName) 75 | { 76 | // Get the module in question. 77 | var module = m_Commands.Modules.FirstOrDefault(x => x.Name.ToLower() == moduleName.ToLower()); 78 | 79 | // If null, we chose a bad module. 80 | if (module == null) 81 | { 82 | await ReplyAsync($"The module `{moduleName}` does not exist. Are you sure you typed the right module?"); 83 | await HelpAsync(); // Show the list of modules again. 84 | return; 85 | } 86 | 87 | // Find all it's commands. 88 | var commands = module.Commands.Where(x => !string.IsNullOrWhiteSpace(x.Summary)).GroupBy(x => x.Name).Select(x => x.First()); 89 | 90 | // If none of them have summaries or don't exist, return. 91 | if (!commands.Any()) 92 | { 93 | await ReplyAsync($"The module `{module.Name}` has no available commands."); 94 | return; 95 | } 96 | 97 | // Create an embed builder. 98 | var emb = new EmbedBuilder(); 99 | 100 | // For each command... 101 | foreach (var command in commands) 102 | { 103 | var result = await command.CheckPreconditionsAsync(Context, m_Provider); 104 | if (result.IsSuccess) 105 | { 106 | var remarks = $"{Config.Instance.Prefix}{command.Remarks}"; 107 | var alias = command.Aliases.First(); 108 | var title = m_UseRemarks ? remarks : alias; 109 | emb.AddField(title, command.Summary); 110 | } 111 | } 112 | 113 | // Reply with a list of commands. 114 | if (emb.Fields.Count <= 0) // Added error checking in case we don't have summary tags yet. 115 | await ReplyAsync("Command information cannot be found, please try again later."); 116 | else 117 | await ReplyAsync("", false, emb.Build()); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Program.cs: -------------------------------------------------------------------------------- 1 | namespace WhalesFargo 2 | { 3 | public class Program 4 | { 5 | // Create a mutex for a single instance. 6 | private static System.Threading.Mutex INSTANCE_MUTEX = new System.Threading.Mutex(true, "WhalesFargo_DiscordBot"); 7 | private static DiscordBot BOT = new DiscordBot(); 8 | public static UI.Window UI = new UI.Window(BOT); 9 | static void Main(string[] args) 10 | { 11 | // Check if an instance is already running. Remove this block if you want to run multiple instances. 12 | if (!INSTANCE_MUTEX.WaitOne(System.TimeSpan.Zero, false)) 13 | { 14 | System.Windows.Forms.MessageBox.Show("The application is already running."); 15 | return; 16 | } 17 | // Start the UI. 18 | try { System.Windows.Forms.Application.Run(UI as System.Windows.Forms.Form); } 19 | catch { System.Console.WriteLine("Failed to run."); } 20 | } 21 | // Connect to the bot, or cancel before the connection happens. 22 | public static void Run() => System.Threading.Tasks.Task.Run(() => BOT.RunAsync()); 23 | public static void Cancel() => System.Threading.Tasks.Task.Run(() => BOT.CancelAsync()); 24 | public static void Stop() => System.Threading.Tasks.Task.Run(() => BOT.StopAsync()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("WhalesFargo")] 9 | [assembly: AssemblyDescription("Discord Bot Written in C#")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("WhalesFargo")] 13 | [assembly: AssemblyCopyright("Copyright © 2017")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Setting ComVisible to false makes the types in this assembly not visible 18 | // to COM components. If you need to access a type in this assembly from 19 | // COM, set the ComVisible attribute to true on that type. 20 | [assembly: ComVisible(false)] 21 | 22 | // The following GUID is for the ID of the typelib if this project is exposed to COM 23 | [assembly: Guid("8cc867bf-9384-41b8-b7dc-17b82e790dc1")] 24 | 25 | // Version information for an assembly consists of the following four values: 26 | // 27 | // Major Version 28 | // Minor Version 29 | // Build Number 30 | // Revision 31 | // 32 | // You can specify all the values or you can default the Build and Revision Numbers 33 | // by using the '*' as shown below: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | -------------------------------------------------------------------------------- /src/Properties/app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 48 | 55 | 56 | 70 | -------------------------------------------------------------------------------- /src/Services/AdminService.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using System.Threading.Tasks; 3 | using WhalesFargo.Helpers; 4 | 5 | namespace WhalesFargo.Services 6 | { 7 | /** 8 | * AdminService 9 | * Simple service for performing administrative functions. This is typically for admin users. 10 | * We place simple restrictions on this, but can be handled in any way. 11 | */ 12 | public class AdminService : CustomService 13 | { 14 | // Private variables. 15 | // TODO: Add any here. 16 | 17 | // Changes the prefix. 18 | public async Task ChangePrefix(char prefix) 19 | { 20 | Config.Instance.Prefix = prefix; 21 | Config.Instance.Write(); // Update local file 22 | Log($"Prefix has been changed to {prefix}", (int)E_LogOutput.Reply); 23 | await Task.Delay(0); 24 | } 25 | 26 | // Mutes the specific user. 27 | public async Task MuteUser(IGuild guild, IUser user) 28 | { 29 | try 30 | { 31 | await (user as IGuildUser).ModifyAsync(x => x.Mute = true); 32 | Log($"{user.Mention} has been muted.", (int)E_LogOutput.Reply); 33 | } 34 | catch 35 | { 36 | Log($"Error while trying to mute {user}."); 37 | } 38 | } 39 | 40 | // Unmutes the specific user. 41 | public async Task UnmuteUser(IGuild guild, IUser user) 42 | { 43 | try 44 | { 45 | await (user as IGuildUser).ModifyAsync(x => x.Mute = false); 46 | Log($"{user.Mention} has been unmuted.", (int)E_LogOutput.Reply); 47 | } 48 | catch 49 | { 50 | Log($"Error while trying to unmute {user}."); 51 | } 52 | } 53 | 54 | // Kicks the specific user. 55 | public async Task KickUser(IGuild guild, IUser user, string reason = null) 56 | { 57 | try 58 | { 59 | await (user as IGuildUser).KickAsync(reason); 60 | } 61 | catch 62 | { 63 | Log($"Error while trying to kick {user}."); 64 | } 65 | } 66 | 67 | // Bans the specific user. 68 | public async Task BanUser(IGuild guild, IUser user, string reason = null) 69 | { 70 | try 71 | { 72 | await guild.AddBanAsync(user, 0, reason); 73 | } 74 | catch 75 | { 76 | Log($"Error while trying to ban {user}."); 77 | } 78 | } 79 | 80 | // From the list of roles, find a role by name. 81 | private IRole FindRole(IGuild guild, string name) 82 | { 83 | var roles = guild.Roles; 84 | foreach (IRole role in roles) 85 | { 86 | if (role.Name.Equals(name)) 87 | return role; 88 | } 89 | return null; 90 | } 91 | 92 | // Create a new role by name, given that it doesn't exist already. 93 | private async Task CreateRole(IGuild guild, string name) 94 | { 95 | // Let's see if the role exists. 96 | var role = FindRole(guild, name); 97 | if (role == null) 98 | role = await guild.CreateRoleAsync(name, GuildPermissions.All); 99 | } 100 | 101 | // Adds a role by name to the user's roles. 102 | public async Task AddRoleUser(IGuild guild, IUser user, string name) 103 | { 104 | var role = FindRole(guild, name); 105 | try 106 | { 107 | if (role != null) await (user as IGuildUser).AddRoleAsync(role); 108 | } 109 | catch 110 | { 111 | Log($"Error while trying to add the role {name} to {user}."); 112 | } 113 | } 114 | 115 | // Removes a role by name from the user's roles. 116 | public async Task RemoveRoleUser(IGuild guild, IUser user, string name) 117 | { 118 | var role = FindRole(guild, name); 119 | try 120 | { 121 | if (role != null) await (user as IGuildUser).RemoveRoleAsync(role); 122 | } 123 | catch 124 | { 125 | Log($"Error while trying to remove the role {name} to {user}."); 126 | } 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Services/AudioService.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using Discord.Audio; 3 | using System; 4 | using System.Collections.Concurrent; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using WhalesFargo.Helpers; 10 | 11 | namespace WhalesFargo.Services 12 | { 13 | /** 14 | * AudioService 15 | * This handles the entire audio service functionality. 16 | * This service used to perform all the tasks required by the module, but most have been separated 17 | * into helper functions. 18 | * 19 | * AudioDownloader handles reading simple meta data from network links and local songs. If specified, 20 | * it'll download network songs into a default folder. 21 | * 22 | * AudioPlayer handles the local and network streams then passes it into FFmpeg to output to the voice channel. 23 | * 24 | * Right now the playlist is maintained in the service, but may be abstracted or moved into another 25 | * class in the future. 26 | */ 27 | public class AudioService : CustomService 28 | { 29 | // Concurrent dictionary for multithreaded environments. 30 | private readonly ConcurrentDictionary m_ConnectedChannels = new ConcurrentDictionary(); 31 | 32 | // Playlist. 33 | private readonly ConcurrentQueue m_Playlist = new ConcurrentQueue(); 34 | 35 | // Downloader. 36 | private readonly AudioDownloader m_AudioDownloader = new AudioDownloader(); // Only downloaded on playlist add. 37 | 38 | // Player. 39 | private readonly AudioPlayer m_AudioPlayer = new AudioPlayer(); 40 | 41 | // Private variables. 42 | private int m_NumPlaysCalled = 0; // This is to check for the last 'ForcePlay' call. 43 | private int m_DelayActionLength = 10000; // To prevent connection issues, we set it to a fairly 'large' value. 44 | private bool m_DelayAction = false; // Temporary Semaphore to control leaving and joining too quickly. 45 | private bool m_AutoPlay = false; // Flag to check if autoplay is currently on or not. 46 | private bool m_AutoPlayRunning = false; // Flag to check if autoplay is currently running or not. More of a 'sanity' check really. 47 | private bool m_AutoDownload = true; // Flag to auto download network items in the playlist. 48 | private bool m_AutoStop = false; // Flag to stop the autoplay service when we're done playing all songs in the playlist. 49 | private Timer m_VoiceChannelTimer = null; // Timer to check for active users in the voice channel. 50 | private bool m_LeaveWhenEmpty = true; // Flag to set up leaving the channel when there are no active users. 51 | 52 | // Using the flag as a semaphore, we pass in a function to lock in between it. Added for better practice. 53 | // Any async function that's called after this, if required can check for m_DelayAction before continuing. 54 | private async Task DelayAction(Action f) 55 | { 56 | m_DelayAction = true; // Lock. 57 | f(); 58 | await Task.Delay(m_DelayActionLength); // Delay to prevent error condition. TEMPORARY. 59 | m_DelayAction = false; // Unlock. 60 | } 61 | 62 | // Gets m_DelayAction, this is a temporary semaphore to prevent joining too quickly after leaving a channel. 63 | public bool GetDelayAction() 64 | { 65 | if (m_DelayAction) Log("This action is delayed. Please try again later."); 66 | return m_DelayAction; 67 | } 68 | 69 | // Joins the voice channel of the target. 70 | // Adds a new client to the ConcurrentDictionary. 71 | public async Task JoinAudioAsync(IGuild guild, IVoiceChannel target) 72 | { 73 | // We can't connect to an empty guilds or targets. 74 | if (guild == null || target == null) return; 75 | 76 | // Delayed join if the client recently left a voice channel. This is to prevent reconnection issues. 77 | if (m_DelayAction) 78 | { 79 | Log("The client is currently disconnecting from a voice channel. Please try again later."); 80 | return; 81 | } 82 | 83 | // Try to get the current audio client. If it's already there, we've already joined. 84 | if (m_ConnectedChannels.TryGetValue(guild.Id, out var connectedAudioClient)) 85 | { 86 | Log("The client is already connected to the current voice channel."); 87 | return; 88 | } 89 | 90 | // If the target guild id doesn't match the guild id we want, return. 91 | // This will likely never happen, but the source message could refer to the incorrect server. 92 | if (target.Guild.Id != guild.Id) 93 | { 94 | Log("Are you sure the current voice channel is correct?"); 95 | return; 96 | } 97 | 98 | // Attempt to connect to this audio channel. 99 | var audioClient = await target.ConnectAsync(); 100 | 101 | try // We should put a try block in case audioClient is null or some other error occurs. 102 | { 103 | // Once connected, add it to the dictionary of connected channels. 104 | if (m_ConnectedChannels.TryAdd(guild.Id, audioClient)) 105 | { 106 | Log("The client is now connected to the current voice channel."); 107 | 108 | // Start check to see if anyone is even in the channel. 109 | if (m_LeaveWhenEmpty) 110 | m_VoiceChannelTimer = new Timer(CheckVoiceChannelState, target, TimeSpan.FromMinutes(10), TimeSpan.FromMinutes(10)); 111 | 112 | return; 113 | } 114 | } 115 | catch 116 | { 117 | Log("The client failed to connect to the target voice channel."); 118 | } 119 | 120 | // If we can't add it to the dictionary or connecting didn't work properly, error. 121 | Log("Unable to join the current voice channel."); 122 | } 123 | 124 | // Leaves the current voice channel. 125 | // Removes the client from the ConcurrentDictionary. 126 | public async Task LeaveAudioAsync(IGuild guild) 127 | { 128 | // We can't disconnect from an empty guild. 129 | if (guild == null) return; 130 | 131 | // To avoid any issues, we stop the player before leaving the channel. 132 | if (m_AudioPlayer.IsRunning()) StopAudio(); 133 | while (m_AudioPlayer.IsRunning()) await Task.Delay(1000); // Wait until it's fully stopped. 134 | 135 | // Attempt to remove from the current dictionary, and if removed, stop it. 136 | if (m_ConnectedChannels.TryRemove(guild.Id, out var audioClient)) 137 | { 138 | Log("The client is now disconnected from the current voice channel."); 139 | await DelayAction(() => audioClient.StopAsync()); // Wait until the audioClient is properly disconnected. 140 | return; 141 | } 142 | 143 | // If we can't remove it from the dictionary, error. 144 | Log("Unable to disconnect from the current voice channel. Are you sure that it is currently connected?"); 145 | } 146 | 147 | // Checks the current status of the voice channel and leaves when empty. 148 | private async void CheckVoiceChannelState(object state) 149 | { 150 | // We can't check anything if the client is null. 151 | if (!(state is IVoiceChannel channel)) return; 152 | 153 | // Check user count. 154 | int count = (await channel.GetUsersAsync().FlattenAsync()).Count(); 155 | if (count < 2) 156 | { 157 | await LeaveAudioAsync(channel.Guild); 158 | if (m_VoiceChannelTimer != null) 159 | { 160 | m_VoiceChannelTimer.Dispose(); 161 | m_VoiceChannelTimer = null; 162 | } 163 | } 164 | } 165 | 166 | // Returns the number of async calls to ForcePlayAudioSync. 167 | public int GetNumPlaysCalled() { return m_NumPlaysCalled; } 168 | 169 | // Force Play the current audio in the voice channel of the target. 170 | // TODO: Consider adding it to autoplay list if it is already playing. 171 | public async Task ForcePlayAudioAsync(IGuild guild, IMessageChannel channel, string path) 172 | { 173 | // We can't play from an empty guild. 174 | if (guild == null) return; 175 | 176 | // Get audio info. 177 | AudioFile song = await GetAudioFileAsync(path); 178 | 179 | // Can't play an empty song. 180 | if (song == null) return; 181 | 182 | // We can only resume autoplay on the last 'play' wait loop. We have to check other 'play's haven't been called. 183 | Interlocked.Increment(ref m_NumPlaysCalled); 184 | 185 | // To avoid any issues, we stop any other audio running. The audioplayer will also stop the current song... 186 | if (m_AudioPlayer.IsRunning()) StopAudio(); 187 | while (m_AudioPlayer.IsRunning()) await Task.Delay(1000); 188 | 189 | // Start the stream, this is the main part of 'play' 190 | if (m_ConnectedChannels.TryGetValue(guild.Id, out var audioClient)) 191 | { 192 | Log($"Now Playing: {song.Title}", (int)E_LogOutput.Reply); // Reply in the text channel. 193 | Log(song.Title, (int)E_LogOutput.Playing); // Set playing. 194 | await m_AudioPlayer.Play(audioClient, song); // The song should already be identified as local or network. 195 | Log(Strings.NotPlaying, (int)E_LogOutput.Playing); 196 | } 197 | else 198 | { 199 | // If we can't get it from the dictionary, we're probably not connected to it yet. 200 | Log("Unable to play in the proper channel. Make sure the audio client is connected."); 201 | } 202 | 203 | // Uncount this play. 204 | Interlocked.Decrement(ref m_NumPlaysCalled); 205 | } 206 | 207 | // This is for the autoplay function which waits after each playback and pulls from the playlist. 208 | // Since the playlist extracts the audio information, we can safely assume that it's chosen the local 209 | // if it exists, or just uses the network link. 210 | public async Task AutoPlayAudioAsync(IGuild guild, IMessageChannel channel) 211 | { 212 | // We can't play from an empty guild. 213 | if (guild == null) return; 214 | 215 | if (m_AutoPlayRunning) return; // Only allow one instance of autoplay. 216 | while (m_AutoPlayRunning = m_AutoPlay) 217 | { 218 | // If the audio player is already playing, we need to wait until it's fully finished. 219 | if (m_AudioPlayer.IsRunning()) await Task.Delay(1000); 220 | 221 | // We do some checks before entering this loop. 222 | if (m_Playlist.IsEmpty || !m_AutoPlayRunning || !m_AutoPlay) break; 223 | 224 | // If there's nothing playing, start the stream, this is the main part of 'play' 225 | if (m_ConnectedChannels.TryGetValue(guild.Id, out var audioClient)) 226 | { 227 | AudioFile song = PlaylistNext(); // If null, nothing in the playlist. We can wait in this loop until there is. 228 | if (song != null) 229 | { 230 | Log($"Now Playing: {song.Title}", (int)E_LogOutput.Reply); // Reply in the text channel. 231 | Log(song.Title, (int)E_LogOutput.Playing); // Set playing. 232 | await m_AudioPlayer.Play(audioClient, song); // The song should already be identified as local or network. 233 | Log(Strings.NotPlaying, (int)E_LogOutput.Playing); 234 | } 235 | else 236 | Log($"Cannot play the audio source specified : {song}"); 237 | 238 | // We do the same checks again to make sure we exit right away. May not be necessary, but let's check anyways. 239 | if (m_Playlist.IsEmpty || !m_AutoPlayRunning || !m_AutoPlay) break; 240 | 241 | // Is null or done with playback. 242 | continue; 243 | } 244 | 245 | // If we can't get it from the dictionary, we're probably not connected to it yet. 246 | Log("Unable to play in the proper channel. Make sure the audio client is connected."); 247 | break; 248 | } 249 | 250 | // Stops autoplay once we're done with it. 251 | if (m_AutoStop) m_AutoPlay = false; 252 | m_AutoPlayRunning = false; 253 | } 254 | 255 | // Returns if the audio player is currently playing or not. 256 | public bool IsAudioPlaying() { return m_AudioPlayer.IsPlaying(); } 257 | 258 | // AudioPlayback Functions. Pause, Resume, Stop, AdjustVolume. 259 | public void PauseAudio() { m_AudioPlayer.Pause(); } 260 | public void ResumeAudio() { m_AudioPlayer.Resume(); } 261 | public void StopAudio() { m_AutoPlay = false; m_AutoPlayRunning = false; m_AudioPlayer.Stop(); } 262 | public void AdjustVolume(float volume) { m_AudioPlayer.AdjustVolume(volume); } // Takes in a value from [0.0f - 1.0f]. 263 | 264 | // Sets the autoplay service to be true. Likely, wherever this is set, we also check and start auto play. 265 | public void SetAutoPlay(bool enable) { m_AutoPlay = enable; } 266 | 267 | // Returns the current state of the autoplay service. 268 | public bool GetAutoPlay() { return m_AutoPlay; } 269 | 270 | // Checks if autoplay is true, but not started yet. If not started, we start autoplay here. 271 | public async Task CheckAutoPlayAsync(IGuild guild, IMessageChannel channel) 272 | { 273 | if (m_AutoPlay && !m_AutoPlayRunning && !m_AudioPlayer.IsRunning()) // if autoplay or force play isn't playing. 274 | await AutoPlayAudioAsync(guild, channel); 275 | } 276 | 277 | // Prints the playlist information. 278 | public void PrintPlaylist() 279 | { 280 | // If none, we return. 281 | int count = m_Playlist.Count; 282 | if (count == 0) 283 | { 284 | Log("There are currently no items in the playlist.", (int)E_LogOutput.Reply); 285 | return; 286 | } 287 | 288 | // Count the number of total digits. 289 | int countDigits = (int)(Math.Floor(Math.Log10(count) + 1)); 290 | 291 | // Create an embed builder. 292 | var emb = new EmbedBuilder(); 293 | 294 | for (int i = 0; i < count; i++) 295 | { 296 | // Prepend 0's so it matches in length. 297 | string zeros = ""; 298 | int numDigits = (i == 0) ? 1 : (int)(Math.Floor(Math.Log10(i) + 1)); 299 | while (numDigits < countDigits) 300 | { 301 | zeros += "0"; 302 | ++numDigits; 303 | } 304 | 305 | // Filename. 306 | AudioFile current = m_Playlist.ElementAt(i); 307 | emb.AddField(zeros + i, current); 308 | } 309 | 310 | DiscordReply("Playlist", emb); 311 | } 312 | 313 | // Adds a song to the playlist. 314 | public async Task PlaylistAddAsync(string path) 315 | { 316 | // Get audio info. 317 | AudioFile audio = await GetAudioFileAsync(path); 318 | if (audio != null) 319 | { 320 | m_Playlist.Enqueue(audio); // Only add if there's no errors. 321 | Log($"Added to playlist : {audio.Title}", (int)E_LogOutput.Reply); 322 | 323 | // If the downloader is set to true, we start the autodownload helper. 324 | if (m_AutoDownload) 325 | { 326 | if (audio.IsNetwork) m_AudioDownloader.Push(audio); // Auto download while in playlist. 327 | await m_AudioDownloader.StartDownloadAsync(); // Start the downloader if it's off. 328 | } 329 | } 330 | } 331 | 332 | // Gets the next song in the playlist queue. 333 | private AudioFile PlaylistNext() 334 | { 335 | if (m_Playlist.TryDequeue(out AudioFile nextSong)) 336 | return nextSong; 337 | 338 | if (m_Playlist.Count <= 0) Log("We reached the end of the playlist."); 339 | else Log("The next song could not be opened."); 340 | return nextSong; 341 | } 342 | 343 | // Skips the current playlist song if autoplay is on. 344 | public void PlaylistSkip() 345 | { 346 | if (!m_AutoPlay) 347 | { 348 | Log("Autoplay service hasn't been started."); 349 | return; 350 | } 351 | if (!m_AudioPlayer.IsRunning()) 352 | { 353 | Log("There's no audio currently playing."); 354 | return; 355 | } 356 | m_AudioPlayer.Stop(); 357 | } 358 | 359 | // Extracts simple meta data from the path and fills a new AudioFile 360 | // information about the audio source. If it fails in the downloader or here, 361 | // we simply return null. 362 | private async Task GetAudioFileAsync(string path) 363 | { 364 | try // We put this in a try catch block. 365 | { 366 | AudioFile song = await m_AudioDownloader.GetAudioFileInfo(path); 367 | if (song != null) // We check for a local available version. 368 | { 369 | string filename = m_AudioDownloader.GetItem(song.Title); 370 | if (filename != null) // We found a local version. 371 | { 372 | song.FileName = filename; 373 | song.IsNetwork = false; // Network is now false. 374 | song.IsDownloaded = true; 375 | } 376 | } 377 | return song; 378 | } 379 | catch 380 | { 381 | return null; 382 | } 383 | } 384 | 385 | // Finds all the local songs and prints out a set at a time by page number. 386 | public void PrintLocalSongs(int page) 387 | { 388 | // Get all the songs in this directory. 389 | string[] items = m_AudioDownloader.GetAllItems(); 390 | int itemCount = items.Length; 391 | if (itemCount == 0) 392 | { 393 | Log("No local files found.", (int)E_LogOutput.Reply); 394 | return; 395 | } 396 | 397 | // Count the number of total digits. 398 | int countDigits = (int)(Math.Floor(Math.Log10(items.Length) + 1)); 399 | 400 | // Set pages to print. 401 | int pageSize = 20; 402 | int pages = (itemCount / pageSize) + 1; 403 | if (page < 1 || page > pages) 404 | { 405 | Log($"There are {pages} pages. Select page 1 to {pages}.", (int)E_LogOutput.Reply); 406 | return; 407 | } 408 | 409 | // Start printing. 410 | for (int p = page - 1; p < page; p++) 411 | { 412 | // Create an embed builder. 413 | var emb = new EmbedBuilder(); 414 | 415 | for (int i = 0; i < pageSize; i++) 416 | { 417 | // Get the index for the file. 418 | int index = (p * pageSize) + i; 419 | if (index >= itemCount) break; 420 | 421 | // Prepend 0's so it matches in length. This will be the 'index'. 422 | string zeros = ""; 423 | int numDigits = (index == 0) ? 1 : (int)(Math.Floor(Math.Log10(index) + 1)); 424 | while (numDigits < countDigits) 425 | { 426 | zeros += "0"; 427 | ++numDigits; 428 | } 429 | 430 | // Filename. 431 | string file = items[index].Split(Path.DirectorySeparatorChar).Last(); // Get just the file name. 432 | emb.AddField(zeros + index, file); 433 | } 434 | 435 | DiscordReply($"Page {p+1}", emb); 436 | } 437 | } 438 | 439 | // Sets the audio download path. This should only be called during init. 440 | public void SetDownloadPath(string path) { m_AudioDownloader.SetDownloadPath(path);} 441 | 442 | // Returns the name with the specified song by index. 443 | // Returns null if a local song doesn't exist. 444 | public string GetLocalSong(int index) { return m_AudioDownloader.GetItem(index); } 445 | 446 | // Adds a song to the download queue. 447 | public async Task DownloadSongAsync(string path) 448 | { 449 | AudioFile audio = await GetAudioFileAsync(path); 450 | if (audio != null) 451 | { 452 | Log($"Added to the download queue : {audio.Title}", (int)E_LogOutput.Reply); 453 | 454 | // If the downloader is set to true, we start the autodownload helper. 455 | if (audio.IsNetwork) m_AudioDownloader.Push(audio); // Auto download while in playlist. 456 | await m_AudioDownloader.StartDownloadAsync(); // Start the downloader if it's off. 457 | } 458 | } 459 | 460 | // Removes any duplicates in our download folder. 461 | public async Task RemoveDuplicateSongsAsync() 462 | { 463 | m_AudioDownloader.RemoveDuplicateItems(); 464 | await Task.Delay(0); 465 | } 466 | 467 | } 468 | } 469 | -------------------------------------------------------------------------------- /src/Services/ChatService.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using System.Threading.Tasks; 3 | 4 | namespace WhalesFargo.Services 5 | { 6 | /** 7 | * ChatService 8 | * Handles the simple chat services like responses and manipulating chat text. 9 | */ 10 | public class ChatService : CustomService 11 | { 12 | // Replies in the text channel using the parent module. 13 | public void SayMessage(string s) 14 | { 15 | DiscordReply(s); 16 | } 17 | 18 | // Sets the bot playing status. 19 | public void SetStatus(string s) 20 | { 21 | DiscordPlaying(s); 22 | } 23 | 24 | // Clears [num] number of messages from the current text channel. 25 | public async Task ClearMessagesAsync(IGuild guild, IMessageChannel channel, IUser user , int num) 26 | { 27 | // Check usage case. 28 | if (num == 0) // Check if Delete is 0, int cannot be null. 29 | { 30 | Log("You need to specify the amount | !clear (amount) | Replace (amount) with anything", (int)E_LogOutput.Reply); 31 | return; 32 | } 33 | 34 | // Check permissions. 35 | var GuildUser = await guild.GetUserAsync(user.Id); 36 | if (!GuildUser.GetPermissions(channel as ITextChannel).ManageMessages) 37 | { 38 | Log("You do not have enough permissions to manage messages", (int)E_LogOutput.Reply); 39 | return; 40 | } 41 | 42 | // Delete. 43 | var messages = await channel.GetMessagesAsync((int)num + 1).FlattenAsync(); 44 | foreach(IMessage m in messages) 45 | { 46 | await channel.DeleteMessageAsync(m.Id); 47 | } 48 | 49 | 50 | // Reply with status. 51 | Log($"{user.Username} deleted {num} messages", (int)E_LogOutput.Reply); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/Services/CustomService.cs: -------------------------------------------------------------------------------- 1 | using Discord; 2 | using System; 3 | using WhalesFargo.Modules; 4 | 5 | namespace WhalesFargo.Services 6 | { 7 | // Enum to direct the string to output. Reference Log() 8 | public enum E_LogOutput { Console, Reply, Playing }; 9 | 10 | /** 11 | * CustomService 12 | * Class that handles serves as a wrapper for services. 13 | * Add shared functionality here and shared properties between all services. 14 | * This should be paired with the CustomModule to use these functions. 15 | */ 16 | public class CustomService 17 | { 18 | // We have a reference to the parent module to perform actions 19 | // like replying and setting the current game properly. 20 | private CustomModule m_ParentModule = null; 21 | 22 | // This should always be called in the module constructor to 23 | // provide a direct reference to the parent module. 24 | public void SetParentModule(CustomModule parent) { m_ParentModule = parent; } 25 | 26 | // Replies in the text channel using the parent module and optional embed. 27 | protected async void DiscordReply(string s, EmbedBuilder emb = null) 28 | { 29 | if (m_ParentModule == null) return; 30 | if (emb != null) 31 | await m_ParentModule.ServiceReplyAsync(s, emb); 32 | else 33 | await m_ParentModule.ServiceReplyAsync(s); 34 | } 35 | 36 | // Sets the playing status using the parent module. 37 | protected async void DiscordPlaying(string s) 38 | { 39 | if (m_ParentModule == null) return; 40 | await m_ParentModule.ServicePlayingAsync(s); 41 | } 42 | 43 | // A Custom logger which can send messages to 44 | // console, reply in module, or set to playing. 45 | // By default, we log everything to the console. 46 | // TODO: Configure as OR flags to set multiple options. 47 | protected void Log(string s, int output = (int)E_LogOutput.Console) 48 | { 49 | string withDate = $"{DateTime.Now.ToString("hh:mm:ss")} DiscordBot {s}"; 50 | #if (DEBUG_VERBOSE) 51 | Console.WriteLine("[DEBUG] -- " + str); 52 | #endif 53 | if (output == (int)E_LogOutput.Console) 54 | { 55 | if (Program.UI != null) Program.UI.SetConsoleText(withDate); 56 | Console.WriteLine("DEBUG -- " + withDate); 57 | } 58 | if (output == (int)E_LogOutput.Reply) DiscordReply($"`{s}`"); 59 | if (output == (int)E_LogOutput.Playing) 60 | { 61 | if (Program.UI != null) Program.UI.SetAudioText(s); 62 | DiscordPlaying(s); 63 | } 64 | } 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/UI/Window.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace WhalesFargo.UI 2 | { 3 | partial class Window 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | this.components = new System.ComponentModel.Container(); 32 | System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Window)); 33 | this.ConsoleText = new System.Windows.Forms.TextBox(); 34 | this.AudioLabel = new System.Windows.Forms.Label(); 35 | this.ConnectionStatus = new System.Windows.Forms.Label(); 36 | this.ConnectionButton = new System.Windows.Forms.Button(); 37 | this.ConnectionStatusLabel = new System.Windows.Forms.Label(); 38 | this.SystemTray = new System.Windows.Forms.NotifyIcon(this.components); 39 | this.AudioText = new System.Windows.Forms.Label(); 40 | this.SuspendLayout(); 41 | // 42 | // ConsoleText 43 | // 44 | this.ConsoleText.BackColor = System.Drawing.SystemColors.Window; 45 | this.ConsoleText.Cursor = System.Windows.Forms.Cursors.Default; 46 | this.ConsoleText.ForeColor = System.Drawing.SystemColors.InfoText; 47 | this.ConsoleText.Location = new System.Drawing.Point(10, 85); 48 | this.ConsoleText.Margin = new System.Windows.Forms.Padding(2, 5, 2, 5); 49 | this.ConsoleText.Multiline = true; 50 | this.ConsoleText.Name = "ConsoleText"; 51 | this.ConsoleText.ReadOnly = true; 52 | this.ConsoleText.ScrollBars = System.Windows.Forms.ScrollBars.Vertical; 53 | this.ConsoleText.ShortcutsEnabled = false; 54 | this.ConsoleText.Size = new System.Drawing.Size(310, 115); 55 | this.ConsoleText.TabIndex = 1; 56 | this.ConsoleText.TabStop = false; 57 | // 58 | // AudioLabel 59 | // 60 | this.AudioLabel.Location = new System.Drawing.Point(10, 30); 61 | this.AudioLabel.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); 62 | this.AudioLabel.Name = "AudioLabel"; 63 | this.AudioLabel.Size = new System.Drawing.Size(85, 15); 64 | this.AudioLabel.TabIndex = 3; 65 | this.AudioLabel.Text = "Now Playing :"; 66 | this.AudioLabel.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; 67 | // 68 | // ConnectionStatus 69 | // 70 | this.ConnectionStatus.BackColor = System.Drawing.Color.Red; 71 | this.ConnectionStatus.Location = new System.Drawing.Point(130, 10); 72 | this.ConnectionStatus.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); 73 | this.ConnectionStatus.Name = "ConnectionStatus"; 74 | this.ConnectionStatus.Size = new System.Drawing.Size(93, 15); 75 | this.ConnectionStatus.TabIndex = 5; 76 | this.ConnectionStatus.Text = "Disconnected"; 77 | this.ConnectionStatus.TextAlign = System.Drawing.ContentAlignment.MiddleCenter; 78 | // 79 | // ConnectionButton 80 | // 81 | this.ConnectionButton.Location = new System.Drawing.Point(10, 53); 82 | this.ConnectionButton.Margin = new System.Windows.Forms.Padding(2, 5, 2, 5); 83 | this.ConnectionButton.Name = "ConnectionButton"; 84 | this.ConnectionButton.Size = new System.Drawing.Size(310, 25); 85 | this.ConnectionButton.TabIndex = 7; 86 | this.ConnectionButton.Text = "Connect"; 87 | this.ConnectionButton.UseVisualStyleBackColor = true; 88 | this.ConnectionButton.Click += new System.EventHandler(this.ConnectionButton_Click); 89 | // 90 | // ConnectionStatusLabel 91 | // 92 | this.ConnectionStatusLabel.Location = new System.Drawing.Point(10, 10); 93 | this.ConnectionStatusLabel.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); 94 | this.ConnectionStatusLabel.Name = "ConnectionStatusLabel"; 95 | this.ConnectionStatusLabel.Size = new System.Drawing.Size(115, 15); 96 | this.ConnectionStatusLabel.TabIndex = 8; 97 | this.ConnectionStatusLabel.Text = "Connection Status :"; 98 | // 99 | // SystemTray 100 | // 101 | this.SystemTray.BalloonTipText = "\r\n"; 102 | this.SystemTray.Icon = ((System.Drawing.Icon)(resources.GetObject("SystemTray.Icon"))); 103 | this.SystemTray.Text = "DiscordBot"; 104 | this.SystemTray.DoubleClick += new System.EventHandler(this.SystemTray_DoubleClick); 105 | // 106 | // AudioText 107 | // 108 | this.AudioText.Location = new System.Drawing.Point(100, 30); 109 | this.AudioText.Margin = new System.Windows.Forms.Padding(2, 0, 2, 0); 110 | this.AudioText.Name = "AudioText"; 111 | this.AudioText.Size = new System.Drawing.Size(210, 15); 112 | this.AudioText.TabIndex = 9; 113 | this.AudioText.Text = "Nothing"; 114 | this.AudioText.TextAlign = System.Drawing.ContentAlignment.MiddleLeft; 115 | // 116 | // Window 117 | // 118 | this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F); 119 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 120 | this.BackColor = System.Drawing.Color.SlateGray; 121 | this.BackgroundImageLayout = System.Windows.Forms.ImageLayout.None; 122 | this.ClientSize = new System.Drawing.Size(334, 211); 123 | this.Controls.Add(this.AudioText); 124 | this.Controls.Add(this.ConnectionStatusLabel); 125 | this.Controls.Add(this.ConnectionButton); 126 | this.Controls.Add(this.ConnectionStatus); 127 | this.Controls.Add(this.AudioLabel); 128 | this.Controls.Add(this.ConsoleText); 129 | this.Font = new System.Drawing.Font("Microsoft Sans Serif", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); 130 | this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; 131 | this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); 132 | this.Margin = new System.Windows.Forms.Padding(2, 5, 2, 5); 133 | this.MaximizeBox = false; 134 | this.Name = "Window"; 135 | this.SizeGripStyle = System.Windows.Forms.SizeGripStyle.Hide; 136 | this.Text = "Discord Bot"; 137 | this.Load += new System.EventHandler(this.Window_Load); 138 | this.SizeChanged += new System.EventHandler(this.Window_SizeChanged); 139 | this.ResumeLayout(false); 140 | this.PerformLayout(); 141 | 142 | } 143 | 144 | #endregion 145 | private System.Windows.Forms.TextBox ConsoleText; 146 | private System.Windows.Forms.Label AudioLabel; 147 | private System.Windows.Forms.Label ConnectionStatus; 148 | private System.Windows.Forms.Button ConnectionButton; 149 | private System.Windows.Forms.Label ConnectionStatusLabel; 150 | private System.Windows.Forms.NotifyIcon SystemTray; 151 | private System.Windows.Forms.Label AudioText; 152 | } 153 | } -------------------------------------------------------------------------------- /src/UI/Window.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Forms; 3 | using WhalesFargo.Helpers; 4 | 5 | namespace WhalesFargo.UI 6 | { 7 | /** 8 | * Window 9 | * Main window for all UI functionality. 10 | * 11 | */ 12 | public partial class Window : Form 13 | { 14 | private DiscordBot m_DiscordBot = null; // Reference to the bot. 15 | private Timer m_AudioTextTimer = new Timer(); // Text timer to scroll the audio's title. 16 | private const int m_AudioTextInterval = 600; // Interval for scroll speed (in milliseconds). 17 | 18 | // Constructor. InitializeComponent is for designer support. Without it, we unlink it from the designer. 19 | public Window(DiscordBot bot) 20 | { 21 | InitializeComponent(); 22 | m_DiscordBot = bot; 23 | } 24 | 25 | // On load, we setup audiotimer for scrolling. 26 | private void Window_Load(object sender, EventArgs e) 27 | { 28 | m_AudioTextTimer.Interval = m_AudioTextInterval; 29 | m_AudioTextTimer.Tick += new System.EventHandler(AudioText_Scroll); 30 | } 31 | 32 | // The current window isn't sizable, but when we minimize it, we show it to the system tray. 33 | // This removes it from the taskbar. 34 | private void Window_SizeChanged(object sender, EventArgs e) 35 | { 36 | if (WindowState == FormWindowState.Minimized) 37 | { 38 | Hide(); 39 | SystemTray.Visible = true; 40 | } 41 | } 42 | 43 | // When in system tray, double click to resume the window. 44 | private void SystemTray_DoubleClick(object sender, EventArgs e) 45 | { 46 | Show(); 47 | SystemTray.Visible = false; 48 | WindowState = FormWindowState.Normal; 49 | } 50 | 51 | // The audio text scrolls with the interval set above. 52 | private void AudioText_Scroll(object Sender, EventArgs e) 53 | { 54 | if (AudioText.Text.Length > 0) 55 | AudioText.Text = AudioText.Text.Substring(1, AudioText.Text.Length - 1) + AudioText.Text.Substring(0,1); 56 | } 57 | 58 | // Handles the connection button state. It can be either connect, cancel (which cancels the connection process if failing), 59 | // and disconnect. We run the proper function for each, then send it to another function to handle the ui component. 60 | private void ConnectionButton_Click(object sender, EventArgs e) 61 | { 62 | // If it's already connected, then we stop the connection, then reset the text. 63 | if (DiscordBot.ConnectionStatus.Equals(Strings.Connected)) 64 | { 65 | if (System.Windows.Forms.MessageBox.Show(Strings.DisconnectPrompt, Strings.DisconnectPromptTitle, 66 | MessageBoxButtons.YesNo) == System.Windows.Forms.DialogResult.Yes) 67 | Program.Stop(); 68 | } 69 | 70 | // If it's in the middle of connecting and keeps failing, we can cancel the attempt. 71 | else if (DiscordBot.ConnectionStatus.Equals(Strings.Connecting)) { Program.Cancel(); } 72 | 73 | // Otherwise, we perform a connection 74 | else { Program.Run(); } 75 | 76 | // Update the connection status. 77 | SetConnectionStatus(DiscordBot.ConnectionStatus); 78 | } 79 | 80 | // Sets the current connection status label and color. 81 | public void SetConnectionStatus(string s) 82 | { 83 | if (InvokeRequired) 84 | { 85 | Invoke(new Action(SetConnectionStatus), new object[] { s }); 86 | return; 87 | } 88 | 89 | // Set the status text. 90 | ConnectionStatus.Text = s; 91 | 92 | if (s.Equals(Strings.Disconnected)) 93 | { 94 | ConnectionStatus.BackColor = System.Drawing.Color.Red; 95 | ConnectionButton.Text = Strings.ConnectButton; 96 | } 97 | 98 | if (s.Equals(Strings.Connecting)) 99 | { 100 | ConnectionStatus.BackColor = System.Drawing.Color.Yellow; 101 | ConnectionButton.Text = Strings.CancelButton; 102 | } 103 | if (s.Equals(Strings.Connected)) 104 | { 105 | ConnectionStatus.BackColor = System.Drawing.Color.Green; 106 | ConnectionButton.Text = Strings.DisconnectButton; 107 | } 108 | } 109 | 110 | // Writes out to console. It doesn't set it completely, but appends to the end of the last message. 111 | public void SetConsoleText(string s) 112 | { 113 | if (InvokeRequired) 114 | { 115 | Invoke(new Action(SetConsoleText), new object[] { s }); 116 | return; 117 | } 118 | ConsoleText.AppendText($"{s}\r\n"); 119 | } 120 | 121 | // Sets the currently playing audio text. 122 | public void SetAudioText(string s) 123 | { 124 | if (InvokeRequired) 125 | { 126 | Invoke(new Action(SetAudioText), new object[] { s }); 127 | return; 128 | } 129 | 130 | AudioText.Text = s.PadRight(s.Length + 20); 131 | 132 | // Turn off scroll if we're playing nothing. 133 | if (s.Equals(Strings.NotPlaying)) 134 | { 135 | m_AudioTextTimer.Enabled = false; 136 | return; 137 | } 138 | else if (m_AudioTextTimer.Enabled == false) m_AudioTextTimer.Enabled = true; 139 | 140 | // If desktop notifications and we didn't return, show balloon text. 141 | if (m_DiscordBot != null && m_DiscordBot.GetDesktopNotifications() && SystemTray.Visible) 142 | { 143 | SystemTray.BalloonTipText = s; 144 | SystemTray.ShowBalloonTip(1000); 145 | } 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/WhalesFargo.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {8CC867BF-9384-41B8-B7DC-17B82E790DC1} 8 | WinExe 9 | WhalesFargo 10 | WhalesFargo 11 | v4.6.1 12 | 512 13 | true 14 | 15 | false 16 | 17 | 18 | true 19 | Disk 20 | false 21 | Foreground 22 | 7 23 | Days 24 | false 25 | false 26 | true 27 | true 28 | 4 29 | 1.0.0.%2a 30 | false 31 | true 32 | true 33 | 34 | 35 | 36 | 37 | AnyCPU 38 | true 39 | full 40 | false 41 | bin\Debug\ 42 | DEBUG;TRACE 43 | prompt 44 | 4 45 | true 46 | true 47 | 48 | 49 | AnyCPU 50 | pdbonly 51 | true 52 | bin\Release\ 53 | TRACE 54 | prompt 55 | 4 56 | true 57 | true 58 | 59 | 60 | 065A50AA37BFE5AE6EF99951BCABC28D05E18840 61 | 62 | 63 | false 64 | 65 | 66 | false 67 | 68 | 69 | LocalIntranet 70 | 71 | 72 | Properties\app.manifest 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | ..\packages\Discord.Net.Commands.2.1.1\lib\net46\Discord.Net.Commands.dll 81 | 82 | 83 | ..\packages\Discord.Net.Core.2.1.1\lib\net46\Discord.Net.Core.dll 84 | 85 | 86 | ..\packages\Discord.Net.Rest.2.1.1\lib\net46\Discord.Net.Rest.dll 87 | 88 | 89 | ..\packages\Discord.Net.Webhook.2.1.1\lib\netstandard1.3\Discord.Net.Webhook.dll 90 | 91 | 92 | ..\packages\Discord.Net.WebSocket.2.1.1\lib\net46\Discord.Net.WebSocket.dll 93 | 94 | 95 | 96 | ..\packages\Microsoft.Extensions.DependencyInjection.2.0.0\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.dll 97 | 98 | 99 | ..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.2.0.0\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll 100 | 101 | 102 | ..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll 103 | 104 | 105 | 106 | ..\packages\System.Collections.Immutable.1.4.0\lib\netstandard2.0\System.Collections.Immutable.dll 107 | 108 | 109 | 110 | ..\packages\System.Diagnostics.DiagnosticSource.4.4.1\lib\net46\System.Diagnostics.DiagnosticSource.dll 111 | 112 | 113 | 114 | ..\packages\System.Interactive.Async.3.2.0\lib\net46\System.Interactive.Async.dll 115 | 116 | 117 | ..\packages\System.IO.Compression.4.3.0\lib\net46\System.IO.Compression.dll 118 | True 119 | 120 | 121 | ..\packages\System.Net.Http.4.3.3\lib\net46\System.Net.Http.dll 122 | True 123 | 124 | 125 | ..\packages\System.Net.Sockets.4.3.0\lib\net46\System.Net.Sockets.dll 126 | 127 | 128 | 129 | ..\packages\System.Security.Cryptography.Algorithms.4.3.1\lib\net461\System.Security.Cryptography.Algorithms.dll 130 | True 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | True 144 | True 145 | Strings.resx 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | Form 161 | 162 | 163 | Window.cs 164 | 165 | 166 | 167 | 168 | 169 | PreserveNewest 170 | ffmpeg.exe 171 | 172 | 173 | PreserveNewest 174 | ffplay.exe 175 | 176 | 177 | PreserveNewest 178 | ffprobe.exe 179 | 180 | 181 | PreserveNewest 182 | libsodium.dll 183 | 184 | 185 | PreserveNewest 186 | opus.dll 187 | 188 | 189 | PreserveNewest 190 | libopus.dll 191 | 192 | 193 | PreserveNewest 194 | youtube-dl.exe 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | False 207 | Microsoft .NET Framework 4.6.1 %28x86 and x64%29 208 | true 209 | 210 | 211 | 212 | 213 | PublicResXFileCodeGenerator 214 | Strings.Designer.cs 215 | 216 | 217 | Window.cs 218 | 219 | 220 | 221 | -------------------------------------------------------------------------------- /src/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | --------------------------------------------------------------------------------