├── .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 |
--------------------------------------------------------------------------------