What Is This Project
This was just a quick project to look into the Discord API and the Unofficial Wrapper for .Net. Coupling that with the use of an Open API for Trivia Questions, to create an easy but fun bot.
What Made Me Want To Do This Project
Discord has a large amount bots for a whole set of purposes, and I wanted to see how easy it would be to create one. The main Library for calling the Discord API is written for Python, but given that I don’t write in Python I wanted to find a library in C# that I could use instead. Thats when I found the open-sourced library discord-net/Discord.Net
, and is the unofficial library that I used in this project. The choice to build a Trivia bot came up while brain storming ideas, it’s an easy concept with only 1 command needed.
What I Learnt
Given the size of the project, there wasn’t a massive amount that I learned. However there are a few things worth mentioning.
While coding this, I learned a little about how Discord run their API and integrations. Discord has a RESTful API for dealing with generic requests and tasks, and web-socket connections to subscribe to real-time eventing. In this project I really only used the web-sockets, as the service the bot provides only needs the ability to read and send messages. In order for a Bot to be added to a server, the server owner needs to Authorise it. Discord uses OAuth2 for its authentication and requires an application to be created and then scopes to be assigned.
Once this link has been generated and given to the server owner, they can open it and select it to be added. The dropdown will list all servers that they own, meaning you don’t have to add it to all your servers.
And it will ensure that the server owner knows what the bot has permissions to do, and can even deny it some privileges.
This is a great way of controlling the permissions of Bots and other 3rd party applications making use of the Discord API.
I also learnt how to use the Discord.Net library, and how easy they made it to connect to the web-sockets. The Library gives you the ability to create a DiscordSocketClient
, which uses events to drive the real-time eventing provided by the Discord web-sockets. This made it really easy to hook directly into the MessageReceived
event to inform the bot whenever there was a message sent in an authorised server.
...
_client = new DiscordSocketClient();
_client.Log += Log;
await _client.LoginAsync(TokenType.Bot,
_configuration.GetConnectionString("DiscordToken"));
await _client.StartAsync();
_client.MessageReceived += HandleMessageReceivedEvent;
...
As you can see, the client also uses the async workflow that C# provides. Meaning that you can free up threads when waiting for responses from Discord or any other external source. Bots have their own Token that is unique to just that bot. This is what is used when the bot connects to the Discord API in order for the bot to begin connecting to the web-sockets it needs. Using the LoginAsync
method I can specify the type as bot and then pass through the DiscordToken
connection string from the appsettings file in the project. Once that returns successfully, I can start up the connections and make the bot live using the StartAsync
method.
I like how this client has been created, its simple to pickup and allows for good concurrency and efficiency by pushing for async
all along the way. To split the code up a little I created a Handler class to actually do the logic of calling the open API and structuring the return message.
internal class TriviaHandler
{
private HttpClient _httpClient;
public TriviaHandler(HttpClient httpClient)
{
_httpClient = httpClient;
}
public async Task HandleMessage(SocketMessage message)
{
var messageText = message.ToString();
if (messageText.ToCharArray()[0] == '!' && !message.Author.IsBot)
{
if (messageText.StartsWith("!question"))
{
await SendQuestion(message);
}
}
}
A simple Handler that uses a single instance of HttpClient
to make simple http calls to the open API. The first thing the handler does is check that the message received starts with !
, as that is how I’m specifying the start of a command. This makes the bot easily extendible, in order to add a new command all I need to do is add another if statement. Once the command has been identified, I call a specific method.
private async Task<bool> SendQuestion(SocketMessage message)
{
var result = await _httpClient.GetAsync("https://opentdb.com/api.php?amount=1");
if (!result.IsSuccessStatusCode)
{
return false;
}
var content = await result.Content.ReadAsStringAsync();
var response = JsonSerializer.Deserialize<TriviaResponse>(content, new JsonSerializerOptions { PropertyNameCaseInsensitive=true });
var returnText = FormatReturnText(response);
await message.Channel.SendMessageAsync(text: returnText);
return true;
}
Continuing to make the most of the async chain, I used the Async methods on HttpClient
and the Discord.Net SocketMessage
object. The method simply calls the API to get a random trivia question, deserializes it into a concrete object and sends it back as a formatted message. I had to look into some of the formatting supported by Discord, especially to hide the answer.
private string FormatReturnText(TriviaResponse response)
{
var question = response.Results[0];
var sb = new StringBuilder();
sb.Append("\nTrivia Question!\n\n");
sb.Append($"Category: **{question.Category}**\n");
sb.Append($"Type: **{question.Type}**\n");
sb.Append($"Difficulty: **{question.Difficulty}**\n\n");
sb.Append($"Question: {HttpUtility.HtmlDecode(question.Question)}\n");
var possibleAnswers = new string[question.IncorrectAnswers.Length + 1];
question.IncorrectAnswers.CopyTo(possibleAnswers, 0);
possibleAnswers[question.IncorrectAnswers.Length] = question.CorrectAnswer;
RandomiseArray(possibleAnswers);
sb.Append($"Possible Answers: ");
for (int i = 0; i < possibleAnswers.Length; i++)
{
if (i == 0)
{
sb.Append($"{HttpUtility.HtmlDecode(possibleAnswers[i])}");
continue;
}
sb.Append($", {HttpUtility.HtmlDecode(possibleAnswers[i])}");
}
sb.Append("\n\n");
sb.Append($"Correct Answer:\n");
sb.Append($"|| {HttpUtility.HtmlDecode(question.CorrectAnswer)} ||\n");
return sb.ToString();
}
This method simply takes the concrete object and prints it as a string ready for Discord to display. In order to hide the answer, I used a markdown formatting to class the text as a spoiler. This is done by adding ||
at the start and end of what you want to keep a secret until clicked.
What Improvements Do I Want To Make / What Are The Chances Of That Happening
There are a few things I would like to improve about this bot.
First big issue with this bot is how the answer is revealed. Currently the code just wraps the answer in spoiler markdown, meaning that the user will need to click it before the answer is revealed. This is great for the application because the bot doesn’t need to provide a command for the question and a command for the answer. If I did go for the two commands option, I would have had to store which questions the bot sent to which channels and then search for the answer when the second command was called. That all seemed a little overkill for this project so I believe that the best option is to use the spoiler markdown feature. However, The problem with just wrapping the word is that the word’s length is quite obvious. Below is a screenshot showing this problem:
As you can see, “Dr. No” is such a short answer that it was easy to guess from the other possible answers. Fixing this should be fairly easy, by putting a large amount to spaces after the answer we should be able to quickly obscure the length of the word. If we keep the number of characters or whitespace between the spoiler syntax consistent, then it would become even harder to try and guess the answer. Although this is not a hard change, I’m not going to do it as it confuses the code which is meant to be an example of how to create a simple twitch bot.
Another improvement that I could make, is adding the ability to select the difficulty and category for the question. For this I could extend the !question
command to also look for 2 arguments after it. First one being the difficulty, and the other being the category. I would include a “random” difficulty that ignores the difficulty and allow for category to be empty resulting in a random category. The Open Trivia Database supports the ability to set the category and difficulty by passing it in as query parameters. An example of this is https://opentdb.com/api.php?amount=1&category=11&difficulty=medium
and basically is asking for 1 trivia question for category 11 (Entertainment: Film) and thats medium difficulty. To make this even easier to implement, I would probably have a !qhelp
command or something that can list all the categories and which number they map to, meaning the user only needs to specify the number to get it to work. Not a hard thing to implement, and I may do this to show how to handle commands with arguments.
Conclusion
This was a fun easy project, and I would recommend it to anyone that is looking for a small project with an actual potential use case for the future. There are already lots of Trivia bots out there, but using this as an example you could create any number of bots to do tasks for you. Whether thats something that logs your time on the server, or alerting users when something happens. Hugely extendable and easy to begin and get something working!