diff --git a/Discord.Net.sln b/Discord.Net.sln
index daf902b96..9bb940d8c 100644
--- a/Discord.Net.sln
+++ b/Discord.Net.sln
@@ -1,6 +1,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
-VisualStudioVersion = 15.0.27130.0
+VisualStudioVersion = 15.0.27004.2009
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Core", "src\Discord.Net.Core\Discord.Net.Core.csproj", "{91E9E7BD-75C9-4E98-84AA-2C271922E5C2}"
EndProject
@@ -22,7 +22,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Tests", "test\D
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Webhook", "src\Discord.Net.Webhook\Discord.Net.Webhook.csproj", "{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Discord.Net.Analyzers", "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj", "{BBA8E7FB-C834-40DC-822F-B112CB7F0140}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Discord.Net.Analyzers", "src\Discord.Net.Analyzers\Discord.Net.Analyzers.csproj", "{BBA8E7FB-C834-40DC-822F-B112CB7F0140}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "01_basic_ping_bot", "samples\01_basic_ping_bot\01_basic_ping_bot.csproj", "{F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "02_commands_framework", "samples\02_commands_framework\02_commands_framework.csproj", "{4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -130,6 +136,30 @@ Global
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x64.Build.0 = Release|Any CPU
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x86.ActiveCfg = Release|Any CPU
{BBA8E7FB-C834-40DC-822F-B112CB7F0140}.Release|x86.Build.0 = Release|Any CPU
+ {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|x64.Build.0 = Debug|Any CPU
+ {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Debug|x86.Build.0 = Debug|Any CPU
+ {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|x64.ActiveCfg = Release|Any CPU
+ {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|x64.Build.0 = Release|Any CPU
+ {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|x86.ActiveCfg = Release|Any CPU
+ {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E}.Release|x86.Build.0 = Release|Any CPU
+ {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|x64.Build.0 = Debug|Any CPU
+ {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Debug|x86.Build.0 = Debug|Any CPU
+ {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x64.ActiveCfg = Release|Any CPU
+ {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x64.Build.0 = Release|Any CPU
+ {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x86.ActiveCfg = Release|Any CPU
+ {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -141,6 +171,8 @@ Global
{6BDEEC08-417B-459F-9CA3-FF8BAB18CAC7} = {B0657AAE-DCC5-4FBF-8E5D-1FB578CF3012}
{9AFAB80E-D2D3-4EDB-B58C-BACA78D1EA30} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
{BBA8E7FB-C834-40DC-822F-B112CB7F0140} = {CC3D4B1C-9DE0-448B-8AE7-F3F1F3EC5C3A}
+ {F2FF84FB-F6AD-47E5-9EE5-18206CAE136E} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
+ {4E1F1F40-B1DD-40C9-A4B1-A2046A4C9C76} = {BB59D5B5-E7B0-4BF4-8F82-D14431B2799B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D2404771-EEC8-45F2-9D71-F3373F6C1495}
diff --git a/samples/01_basic_ping_bot/01_basic_ping_bot.csproj b/samples/01_basic_ping_bot/01_basic_ping_bot.csproj
new file mode 100644
index 000000000..5484e3d55
--- /dev/null
+++ b/samples/01_basic_ping_bot/01_basic_ping_bot.csproj
@@ -0,0 +1,12 @@
+
+
+
+ Exe
+ netcoreapp2.0
+
+
+
+
+
+
+
diff --git a/samples/01_basic_ping_bot/Program.cs b/samples/01_basic_ping_bot/Program.cs
new file mode 100644
index 000000000..0fcc52b85
--- /dev/null
+++ b/samples/01_basic_ping_bot/Program.cs
@@ -0,0 +1,69 @@
+using System;
+using System.Threading.Tasks;
+using Discord;
+using Discord.WebSocket;
+
+namespace _01_basic_ping_bot
+{
+ // This is a minimal, barebones example of using Discord.Net
+ //
+ // If writing a bot with commands, we recommend using the Discord.Net.Commands
+ // framework, rather than handling commands yourself, like we do in this sample.
+ //
+ // You can find samples of using the command framework:
+ // - Here, under the 02_commands_framework sample
+ // - https://github.com/foxbot/DiscordBotBase - a barebones bot template
+ // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library
+ class Program
+ {
+ private DiscordSocketClient _client;
+
+ // Discord.Net heavily utilizes TAP for async, so we create
+ // an asynchronous context from the beginning.
+ static void Main(string[] args)
+ => new Program().MainAsync().GetAwaiter().GetResult();
+
+ public async Task MainAsync()
+ {
+ _client = new DiscordSocketClient();
+
+ _client.Log += LogAsync;
+ _client.Ready += ReadyAsync;
+ _client.MessageReceived += MessageReceivedAsync;
+
+ // Tokens should be considered secret data, and never hard-coded.
+ await _client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));
+ await _client.StartAsync();
+
+ // Block the program until it is closed.
+ await Task.Delay(-1);
+ }
+
+ private Task LogAsync(LogMessage log)
+ {
+ Console.WriteLine(log.ToString());
+ return Task.CompletedTask;
+ }
+
+ // The Ready event indicates that the client has opened a
+ // connection and it is now safe to access the cache.
+ private Task ReadyAsync()
+ {
+ Console.WriteLine($"{_client.CurrentUser} is connected!");
+
+ return Task.CompletedTask;
+ }
+
+ // This is not the recommmended way to write a bot - consider
+ // reading over the Commands Framework sample.
+ private async Task MessageReceivedAsync(SocketMessage message)
+ {
+ // The bot should never respond to itself.
+ if (message.Author.Id == _client.CurrentUser.Id)
+ return;
+
+ if (message.Content == "!ping")
+ await message.Channel.SendMessageAsync("pong!");
+ }
+ }
+}
diff --git a/samples/02_commands_framework/02_commands_framework.csproj b/samples/02_commands_framework/02_commands_framework.csproj
new file mode 100644
index 000000000..77fdc65e1
--- /dev/null
+++ b/samples/02_commands_framework/02_commands_framework.csproj
@@ -0,0 +1,17 @@
+
+
+
+ Exe
+ netcoreapp2.0
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/02_commands_framework/Modules/PublicModule.cs b/samples/02_commands_framework/Modules/PublicModule.cs
new file mode 100644
index 000000000..f30dfd73f
--- /dev/null
+++ b/samples/02_commands_framework/Modules/PublicModule.cs
@@ -0,0 +1,63 @@
+using System.IO;
+using System.Threading.Tasks;
+using Discord;
+using Discord.Commands;
+using _02_commands_framework.Services;
+
+namespace _02_commands_framework.Modules
+{
+ // Modules must be public and inherit from an IModuleBase
+ public class PublicModule : ModuleBase
+ {
+ // Dependency Injection will fill this value in for us
+ public PictureService PictureService { get; set; }
+
+ [Command("ping")]
+ [Alias("pong", "hello")]
+ public Task PingAsync()
+ => ReplyAsync("pong!");
+
+ [Command("cat")]
+ public async Task CatAsync()
+ {
+ // Get a stream containing an image of a cat
+ var stream = await PictureService.GetCatPictureAsync();
+ // Streams must be seeked to their beginning before being uploaded!
+ stream.Seek(0, SeekOrigin.Begin);
+ await Context.Channel.SendFileAsync(stream, "cat.png");
+ }
+
+ // Get info on a user, or the user who invoked the command if one is not specified
+ [Command("userinfo")]
+ public async Task UserInfoAsync(IUser user = null)
+ {
+ user = user ?? Context.User;
+
+ await ReplyAsync(user.ToString());
+ }
+
+ // Ban a user
+ [Command("ban")]
+ [RequireContext(ContextType.Guild)]
+ // make sure the user invoking the command can ban
+ [RequireUserPermission(GuildPermission.BanMembers)]
+ // make sure the bot itself can ban
+ [RequireBotPermission(GuildPermission.BanMembers)]
+ public async Task BanUserAsync(IGuildUser user, [Remainder] string reason = null)
+ {
+ await user.Guild.AddBanAsync(user, reason: reason);
+ await ReplyAsync("ok!");
+ }
+
+ // [Remainder] takes the rest of the command's arguments as one argument, rather than splitting every space
+ [Command("echo")]
+ public Task EchoAsync([Remainder] string text)
+ // Insert a ZWSP before the text to prevent triggering other bots!
+ => ReplyAsync('\u200B' + text);
+
+ // 'params' will parse space-separated elements into a list
+ [Command("list")]
+ public Task ListAsync(params string[] objects)
+ => ReplyAsync("You listed: " + string.Join("; ", objects));
+ }
+}
diff --git a/samples/02_commands_framework/Program.cs b/samples/02_commands_framework/Program.cs
new file mode 100644
index 000000000..3fed652d3
--- /dev/null
+++ b/samples/02_commands_framework/Program.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Net.Http;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Discord;
+using Discord.WebSocket;
+using Discord.Commands;
+using _02_commands_framework.Services;
+
+namespace _02_commands_framework
+{
+ // This is a minimal example of using Discord.Net's command
+ // framework - by no means does it show everything the framework
+ // is capable of.
+ //
+ // You can find samples of using the command framework:
+ // - Here, under the 02_commands_framework sample
+ // - https://github.com/foxbot/DiscordBotBase - a barebones bot template
+ // - https://github.com/foxbot/patek - a more feature-filled bot, utilizing more aspects of the library
+ class Program
+ {
+ static void Main(string[] args)
+ => new Program().MainAsync().GetAwaiter().GetResult();
+
+ public async Task MainAsync()
+ {
+ var services = ConfigureServices();
+
+ var client = services.GetRequiredService();
+
+ client.Log += LogAsync;
+ services.GetRequiredService().Log += LogAsync;
+
+ await client.LoginAsync(TokenType.Bot, Environment.GetEnvironmentVariable("token"));
+ await client.StartAsync();
+
+ await services.GetRequiredService().InitializeAsync();
+
+ await Task.Delay(-1);
+ }
+
+ private Task LogAsync(LogMessage log)
+ {
+ Console.WriteLine(log.ToString());
+
+ return Task.CompletedTask;
+ }
+
+ private IServiceProvider ConfigureServices()
+ {
+ return new ServiceCollection()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .AddSingleton()
+ .BuildServiceProvider();
+ }
+ }
+}
diff --git a/samples/02_commands_framework/Services/CommandHandlingService.cs b/samples/02_commands_framework/Services/CommandHandlingService.cs
new file mode 100644
index 000000000..fc253eed3
--- /dev/null
+++ b/samples/02_commands_framework/Services/CommandHandlingService.cs
@@ -0,0 +1,49 @@
+using System;
+using System.Reflection;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Discord;
+using Discord.Commands;
+using Discord.WebSocket;
+
+namespace _02_commands_framework.Services
+{
+ public class CommandHandlingService
+ {
+ private readonly CommandService _commands;
+ private readonly DiscordSocketClient _discord;
+ private readonly IServiceProvider _services;
+
+ public CommandHandlingService(IServiceProvider services)
+ {
+ _commands = services.GetRequiredService();
+ _discord = services.GetRequiredService();
+ _services = services;
+
+ _discord.MessageReceived += MessageReceivedAsync;
+ }
+
+ public async Task InitializeAsync()
+ {
+ await _commands.AddModulesAsync(Assembly.GetEntryAssembly(), _services);
+ }
+
+ public async Task MessageReceivedAsync(SocketMessage rawMessage)
+ {
+ // Ignore system messages, or messages from other bots
+ if (!(rawMessage is SocketUserMessage message)) return;
+ if (message.Source != MessageSource.User) return;
+
+ // This value holds the offset where the prefix ends
+ var argPos = 0;
+ if (!message.HasMentionPrefix(_discord.CurrentUser, ref argPos)) return;
+
+ var context = new SocketCommandContext(_discord, message);
+ var result = await _commands.ExecuteAsync(context, argPos, _services);
+
+ if (result.Error.HasValue &&
+ result.Error.Value != CommandError.UnknownCommand) // it's bad practice to send 'unknown command' errors
+ await context.Channel.SendMessageAsync(result.ToString());
+ }
+ }
+}
diff --git a/samples/02_commands_framework/Services/PictureService.cs b/samples/02_commands_framework/Services/PictureService.cs
new file mode 100644
index 000000000..dda818cc3
--- /dev/null
+++ b/samples/02_commands_framework/Services/PictureService.cs
@@ -0,0 +1,20 @@
+using System.IO;
+using System.Net.Http;
+using System.Threading.Tasks;
+
+namespace _02_commands_framework.Services
+{
+ public class PictureService
+ {
+ private readonly HttpClient _http;
+
+ public PictureService(HttpClient http)
+ => _http = http;
+
+ public async Task GetCatPictureAsync()
+ {
+ var resp = await _http.GetAsync("https://cataas.com/cat");
+ return await resp.Content.ReadAsStreamAsync();
+ }
+ }
+}