loading...

Creating a Discord bot with C#/.Net Core and DSharpPlus

bizzycola profile image Bizzycola Updated on ・8 min read

Introduction

Today I'll be showing you how to setup a basic Discord bot using C#, .net core and DSharpPlus. We'll be using reflection to automatically load module classes with your commands in them.

So, first off, for those who have built discord bots with C# before, you might ask why I didn't elect to use Discord.Net (the more popular discord library). Discord.Net is a great library and it has its own pros and cons in comparison.

The reason I like DSharpPlus is the interactive library allowing you to easily await input and reactions from users, as well as (and this is not really important, just something I like) the ease of sending a "Typing.." message before the actual content. This is useful when you have a command that might take a little while, because the "Typing..." indicator shows the users that your bot has received the command and is processing the result.

Note this post is also on my blog.

Creating the discord application

So, to use create a bot, we must first head over to the Discord developer portal and create an app.

Click the New Application button and give your app a name(you'll be able to set your bots nickname and avatar image later, for now just name it what you'd like and hit Create).

You'll now be presented with a page where you can edit your applications name and description. Feel free to do this, then on the left-hand menu, click the "Bot" link.

You'll now have a mostly blank page that has the option to add a bot to this application. Hit Add Bot and hit yes when it asks you to confirm. Here you can set your bots username and profile picture. Keep this page around, you'll need the Token from the bot page as well as the client ID and token from the General Information page a little later.

Okay, now, to add the bot to your discord server(you must have the Manage Server permission to add a bot), change the CLIENTIDHERE in the following url then visit it in your browser whilst logged into discord. Select your server and add the bot: https://discordapp.com/api/oauth2/authorize?client_id=CLIENTIDHERE&scope=bot&permissions=3072

Creating the Project

I'm going to assume that if you're following this tutorial you have an editor(such as VS or VS Code, etc) and have the latest stable version of .net core installed(at this time I believe it's 2.2).

Start off by CDing to the directory in which you wish to create your project and type dotnet new console. At this point, if you wish, you can ensure it works by also running dotnet restore, dotnet build and dotnet run.

Before we get to coding, we're also going to install the DSharpPlus dependancies. In this guide, I'll be showing you how to use both the CommandsNext and Interactive modules of DSharpPlus so we'll go ahead and add those as well with the following commands:

dotnet add package DSharpPlus
dotnet add package DSharpPlus.CommandsNext
dotnet add package DSharpPlus.Interactivity

Now, we're also going to add Microsoft's configuration libraries as well to ease loading a JSON config file in which we'll store our Discord connection details. Add the packages like so:

dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Binder
dotnet add package Microsoft.Extensions.Configuration.Json

At this point you may also run dotnet restore again but dotnet should probably do that for you when you add the packages.

Getting to the Code

Okay, so both DSharpPlus and Discord.Net both use asynchronous code. Newer versions of C# allow you to make your main method async nowadays so this shouldn't be too much of an issue.

Lets start with Program.cs. Feel free to rename this file/the class to something else before continuing if you wish.

using System;
using System.Linq;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using DSharpPlus;
using DSharpPlus.CommandsNext;
using DSharpPlus.Interactivity;
using Microsoft.Extensions.Configuration;

internal class Program
{
    /* This is the cancellation token we'll use to end the bot if needed(used for most async stuff). */
    private CancellationTokenSource _cts { get; set; }

    /* We'll load the app config into this when we create it a little later. */
    private IConfigurationRoot _config;

    /* These are the discord library's main classes */
    private DiscordClient _discord;
    private CommandsNextModule _commands;
    private InteractivityModule _interactivity;

    /* Use the async main to create an instance of the class and await it(async main is only available in C# 7.1 onwards). */
    static async Task Main(string[] args) => await new Program().InitBot(args);

    async Task InitBot(string[] args)
    {
        try
        {
            Console.WriteLine("[info] Welcome to my bot!");
            _cts = new CancellationTokenSource(); 

            // Load the config file(we'll create this shortly)
            Console.WriteLine("[info] Loading config file..");
            _config = new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("config.json", optional: false, reloadOnChange: true)
                .Build();

            // Create the DSharpPlus client
            Console.WriteLine("[info] Creating discord client..");
            _discord = new DiscordClient(new DiscordConfiguration
            {
                Token = _config.GetValue<string>("discord:token"),
                TokenType = TokenType.Bot
            });

            // Create the interactivity module(I'll show you how to use this later on)
            _interactivity = _discord.UseInteractivity(new InteractivityConfiguration()
            {
                PaginationBehaviour = TimeoutBehaviour.Delete, // What to do when a pagination request times out
                PaginationTimeout = TimeSpan.FromSeconds(30), // How long to wait before timing out
                Timeout = TimeSpan.FromSeconds(30) // Default time to wait for interactive commands like waiting for a message or a reaction
            });

            // Build dependancies and then create the commands module.
            var deps = BuildDeps();
            _commands = _discord.UseCommandsNext(new CommandsNextConfiguration
            {
                StringPrefix = _config.GetValue<string>("discord:CommandPrefix"), // Load the command prefix(what comes before the command, eg "!" or "/") from our config file
                Dependencies = deps // Pass the dependancies
            });

            // TODO: Add command loading!

            RunAsync(args).Wait();
        }
        catch(Exception ex)
        {
            // This will catch any exceptions that occur during the operation/setup of your bot.

            // Feel free to replace this with what ever logging solution you'd like to use.
            // I may do a guide later on the basic logger I implemented in my most recent bot.
            Console.Error.WriteLine(ex.ToString());
        }
    }

     async Task RunAsync(string[] args)
    {
        // Connect to discord's service
        Console.WriteLine("Connecting..");
        await _discord.ConnectAsync();
        Console.WriteLine("Connected!");

        // Keep the bot running until the cancellation token requests we stop
        while (!_cts.IsCancellationRequested)
            await Task.Delay(TimeSpan.FromMinutes(1));
    }

    /* 
     DSharpPlus has dependancy injection for commands, this builds a list of dependancies. 
     We can then access these in our command modules.
    */
    private DependencyCollection BuildDeps()
        {
            using var deps = new DependencyCollectionBuilder();

            deps.AddInstance(_interactivity) // Add interactivity
                .AddInstance(_cts) // Add the cancellation token
                .AddInstance(_config) // Add our config
                .AddInstance(_discord); // Add the discord client

            return deps.Build();
        }
}

Wow, that's a lot of code! I've added comments to help you understand whats going on (If you get confused at any point in this guide, let me know in the comments and I can update it with more information if needed).

Adding the Config File

Okay, now if you ran the bot you'd find a few issues. First off, we don't have a config file yet so lets create one.

In your project directory, create a file entitled config.json.
Now, place the following code into the file and replace the values with those you found on the Discord developer portal(except for the command prefix, set that to the character you want before your commands. Perhaps "!", "$" or "/" will do):

{
  "discord": {
    "token": "BOT TOKEN HERE",
    "appId": "CLIENT ID HERE",
    "appSecret": "CLIENT SECRET HERE",
    "CommandPrefix":  "/" 
  }
}

Now, we need this config file to be copied to our output directory when we build the project. Open the .csproj file in an editor of your choice and add the following ItemGroup inside the tags:

<ItemGroup>
  <None Update="config.json" CopyToOutputDirectory="PreserveNewest" />
</ItemGroup>

You may also set "CopyToOutputDirectory" to "Always" if you don't want it to only copy when the file is updated.

Adding Commands

Okay, now we have our config and our bot would likely launch, but now we need a way to interact with it.

Create a new directory in your project titled "Commands"(or modules).
In that directory, create a file named "IModule.cs". This is the interface our commands will extend from so we can load them via reflection code. Just create a an empty interface titled "IModule" like so:

public interface IModule {}

Now, create another file in the Commands directory and call it BasicCommands.Module.cs
In that module, add the following code:

using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
using DSharpPlus.Entities;
using DSharpPlus.Interactivity;
using System;
using System.Threading.Tasks;

/* Create our class and extend from IModule */
public class BasicCommandsModule : IModule
{
    /* Commands in DSharpPlus.CommandsNext are identified by supplying a Command attribute to a method in any class you've loaded into it. */
    /* The description is just a string supplied when you use the help command included in CommandsNext. */
    [Command("alive")]
    [Description("Simple command to test if the bot is running!")]
    public async Task Alive(CommandContext ctx)
    {
        /* Trigger the Typing... in discord */
        await ctx.TriggerTypingAsync();

        /* Send the message "I'm Alive!" to the channel the message was recieved from */
        await ctx.RespondAsync("I'm alive!");
    }
}

Now head back to Program.cs and we'll add in some fancy reflection code to load in anything that extends from our IModule interface.

In the InitBot method, add the following code just above RunAsync(args).wait();:

Console.WriteLine("[info] Loading command modules..");

var type = typeof(IModule); // Get the type of our interface
var types = AppDomain.CurrentDomain.GetAssemblies() // Get the assemblies associated with our project
    .SelectMany(s => s.GetTypes()) // Get all the types
    .Where(p => type.IsAssignableFrom(p) && !p.IsInterface); // Filter to find any type that can be assigned to an IModule

var typeList = types as Type[] ?? types.ToArray(); // Convert to an array
foreach (var t in typeList)
    _commands.RegisterCommands(t); // Loop through the list and register each command module with CommandsNext

Console.WriteLine($"[info] Loaded {typeList.Count()} modules.");

Okay, now run dotnet build and dotnet run. Your bot should come online and and if you type your command prefix and then alive you should see some output(example with /: /alive)!

Interactivity

So, as I mentioned earlier, I like the ability in DSharpPlus to wait for input from a user after a command has been executed. I'm going to show you a basic example for how to wait for a message from a user and read it.

Head back over to your BasicCommands module and add this new method:

[Command("interact")]
[Description("Simple command to test interaction!")]
public async Task Interact(CommandContext ctx)
{
    /* Trigger the Typing... in discord */
    await ctx.TriggerTypingAsync();

    /* Send the message "I'm Alive!" to the channel the message was recieved from */
    await ctx.RespondAsync("How are you today?");

    var intr = ctx.Client.GetInteractivityModule(); // Grab the interactivity module
    var reminderContent = await intr.WaitForMessageAsync(
        c => c.Author.Id == ctx.Message.Author.Id, // Make sure the response is from the same person who sent the command
        TimeSpan.FromSeconds(60) // Wait 60 seconds for a response instead of the default 30 we set earlier!
    );
    // You can also check for a specific message by doing something like
    // c => c.Content == "something"

    // Null if the user didn't respond before the timeout
    if(reminderContent == null)
    {
        await ctx.RespondAsync("Sorry, I didn't get a response!");
        return;
    }

    // Homework: have this change depending on if they say "good" or "bad", etc.
    await ctx.RespondAsync("Thank you for telling me how you are!");
}

If you want to accept arguments from commands, you can add parameters after the CommandContext(EG:

Interact(CommandContext ctx, int age, [remainder]string fullName);

  • [remainder] just says use take all the rest of the arguments and place them in the string).

Okay, so now if you go run the bot and type /interact it'll ask you how you are and wait a whole minute for a response!

Conclusion

So now you've got this down, feel free to check out the DSharpPlus Documentation on how to use some other features and build yourself a fancy bot!

Let me know what you thought of this. In my bots I've implemented background scheduled tasks, a logger that supports multiple outputs and a reminder command that uses those scheduled tasks to alert the user when a reminder is due.

I've also built game bots with scoring systems, games, leaderboards, etc. So, if you want a guide on how to do anything like those, feel free to let me know in the comments and I might write one up.

Hope you enjoyed, be sure to let me know what awesome bots you create! And if you have feedback on the article that'd be great too, it's my first dev.to article!

Posted on Jul 22 '19 by:

bizzycola profile

Bizzycola

@bizzycola

24 year old Software Dev Student/Musician from Sydney, Australia.

Discussion

markdown guide