Build a Trivia Quiz WhatsApp Bot With Twilio and ASP.NET Core

Written by zadok | Published 2023/09/15
Tech Story Tags: whatsapp-bot | twilio-guide | how-to-create-a-whatsapp-bot | trivia-quiz-whatsapp-bot | asp.net-core-guide | twilio-asp.net-core-project | hackernoon-top-story | web-development | hackernoon-es | hackernoon-hi | hackernoon-zh | hackernoon-vi | hackernoon-fr | hackernoon-pt | hackernoon-ja

TLDRIn this tutorial guide, you will learn how to create a trivia quiz using Twilio for WhatsApp, ASP.NET Core, and The Trivia API.via the TL;DR App

Trivia games provide an engaging and educational experience where you can learn new facts and expand your knowledge across various subjects. Nowadays, Trivia quiz mobile and web applications are the most common go-to areas for such an activity. How about playing a trivia game on WhatsApp?

In this tutorial guide, you will learn how to create a trivia quiz application using Twilio for WhatsApp, ASP.NET Core, and The Trivia API (licensed under CC BY-NC 4.0). The aim of this tutorial is to create a trivia game application that allows users to play and answer multiple-choice questions using WhatsApp. You will leverage sessions in ASP.NET Core to store and retrieve user progress, track score and maintain game state.


To fetch these questions, you will be using The Trivia API, a REST API, that makes it easy for developers to build quiz apps by providing multiple-choice trivia questions. To learn more about the Trivia API, please visit the__Trivia API documentation__.

Prerequisites

To complete this tutorial, you will need:


The source code for this tutorial can be found on GitHub.

Set up a new ASP.NET Core project

To get started, using your shell terminal in a preferred working directory, run the following commands to create a new web API project:

dotnet new webapi -n TwilioWhatsAppTriviaApp --no-openapi

The second command in the snippet above will create a new web API project with the specified name and without OpenAPI (Swagger) support. If you want to use Swagger in the project, just omit --no-openapi in the command above.

Change into the project directory by running this command:

cd TwilioWhatsAppTriviaApp

Install the Twilio Helper library for ASP.NET Core NuGet package:

dotnet add package Twilio.AspNet.Core

This library simplifies working with Twilio webhooks and APIs in an ASP.NET Core application.

Open the project using your preferred IDE. In the Controllers folder, remove the boilerplate template controller file, WeatherForecastController.cs, and also remove the WeatherForcast.cs in the project directory.

Build and run your project to ensure everything you have done so far works well using the following commands:

dotnet build
dotnet run


After successfully running the project, take note of any of the localhost URLs that appear in the debugging console. You can use any of these URLs to set up a publicly accessible local web server using ngrok.

Implement Sessions

Sessions are one of the several ways of storing a user’s data in an ASP.NET Core application. This is essential when you want to retain user data between requests because by default, the HTTP protocol is stateless - that means data is not preserved.

Add the in-memory session provider by modifyingProgram.cs, as shown in the following code:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromSeconds(40);
    options.Cookie.IsEssential = true;
});

var app = builder.Build();

app.UseSession();

app.MapControllers();

app.Run();

AddDistributedMemoryCache() registers the distributed memory cache service. This service provides an in-memory cache that can be used to store and retrieve data across multiple requests or sessions.

AddSession() registers the session services, enabling the application to maintain session state. The options parameter allows you to configure various session-related options. IdleTimeout is used to set the duration of inactivity after which a session will be considered idle. In this case, it is set to 40 seconds. Cookie.IsEssential ensures that the session’s state remains functional and even in scenarios where cookie rejection is enabled.

Session support is enabled by adding the UseSession middleware to the application pipeline, that is, your application gains access to a session object that can be used to store and retrieve data.

Create the Models

Create a new folder, Models in your project directory. Add two model class files, TriviaApiResponse.cs and Question.cs with properties as shown in the following code samples:

using Newtonsoft.Json;

namespace TwilioWhatsAppTriviaApp.Models;

public class TriviaApiResponse
{
    [JsonProperty("category")]
    public string Category { get; set; }

    [JsonProperty("correctAnswer")]
    public string CorrectAnswer { get; set; }

    [JsonProperty("incorrectAnswers")]
    public List<string> IncorrectAnswers { get; set; }

    [JsonProperty("question")]
    public string Question { get; set; }

    [JsonProperty("type")]
    public string? Type { get; set; }

    [JsonProperty("difficulty")]
    public string Difficulty { get; set; }
}

namespace TwilioWhatsAppTriviaApp.Models;

public class Question
{
    public string QuestionText { get; set; }
    public List<(string option, bool isCorrect)> Options { get; set; }
}

The TriviaApiResponse model includes properties that represent the fields of The Trivia API response. The JsonProperty attribute ensures that each property is correctly populated with the corresponding JSON data.

For a streamlined way to handle trivia questions, the Question class comes to the rescue. This class encapsulates the necessary information for a trivia question, including the question text and a list of options. Each option is represented by a tuple containing the option text and a boolean value indicating whether it is the correct option.


Add the Trivia Service Class

Create a Services folder in your project directory and add a new class file named TriviaService.cs. Modify its contents, as shown in the following code:

using Newtonsoft.Json;
using TwilioWhatsAppTriviaApp.Models;

namespace TwilioWhatsAppTriviaApp.Services;

public class TriviaService
{
    private const string TheTriviaApiUrl = @"https://the-trivia-api.com/api/questions?limit=3";
    private HttpClient httpClient;

    public TriviaService(HttpClient httpClient)
    {
        this.httpClient = httpClient;
    }

    public async Task<IEnumerable<TriviaApiResponse>> GetTrivia()
    {
        var response = await httpClient.GetAsync(TheTriviaApiUrl);
        var triviaJson = await response.Content.ReadAsStringAsync();
        var trivia = JsonConvert.DeserializeObject<IEnumerable<TriviaApiResponse>>(triviaJson);

        return trivia;
    }

    public List<Question> ConvertTriviaToQuestions(IEnumerable<TriviaApiResponse> questions)
    {
        List<Question> newQuestions = new();

        foreach (var question in questions)
        {
            var options = new List<(string option, bool isCorrect)>()
            {
                (question.CorrectAnswer, true),
                (question.IncorrectAnswers[0], false),
                (question.IncorrectAnswers[1], false),
                (question.IncorrectAnswers[2], false)
            };

            // Shuffle the options randomly
            Random random = new();
            options = options.OrderBy(_ => random.Next()).ToList();

            newQuestions.Add(new Question { QuestionText = question.Question, Options = options });
        }

        return newQuestions;
    }
}


TheTriviaService class contains two methods: GetTrivia and ConvertTriviaToQuestions. The GetTrivia method sends the HTTP GET request to The Trivia API with a query parameter, limit=3, that specifies that only 3 questions should be returned. Without the limit parameter, the API returns 10 questions by default.

The ConvertTriviaToQuestions method converts the response from the API into an organized manner. The method also shuffles all question options randomly, so that a single option won’t be the answer to all questions.


To registerTriviaService and the HTTP client in your application's Dependency Injection (DI) container, modify Program.cs as shown in the following code:

using TwilioWhatsAppTriviaApp.Services;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromSeconds(40);
    options.Cookie.IsEssential = true;
});
builder.Services.AddHttpClient();
builder.Services.AddScoped<TriviaService>();

var app = builder.Build();

app.UseSession();

app.MapControllers();

app.Run();

Create the Trivia Controller

Add an empty API controller class in a file named TriviaController.cs to the Controllers folder and modify its content as shown in the following code:

using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using Twilio.AspNet.Core;
using Twilio.TwiML;
using Twilio.TwiML.Messaging;
using TwilioWhatsAppTriviaApp.Models;
using TwilioWhatsAppTriviaApp.Services;

namespace WhatsappTrivia.Controllers;

[Route("[controller]")]
[ApiController]
public class TriviaController : TwilioController
{
    private const string SessionKeyIsGameOn = "IsGameOn";
    private const string SessionKeyScore = "Score";
    private const string SessionKeyCurrentQuestionIndex = "CurrentQuestionIndex";
    private const string SessionKeyTotalQuestions = "TotalQuestions";
    private const string SessionKeyQuestions = "Questions";

    private static readonly string[] StartCommands = { "START", "S" };
    private static readonly string[] OptionValues = { "A", "B", "C", "D" };

    private readonly TriviaService triviaService;

    public TriviaController(TriviaService triviaService)
    {
        this.triviaService = triviaService;
    }

    [HttpPost]
    public async Task<IActionResult> Index()
    {
        var response = new MessagingResponse();
        var form = await Request.ReadFormAsync();
        var body = form["Body"].ToString().ToUpper().Trim();

        await HttpContext.Session.LoadAsync();
        var isGameOn = Convert.ToBoolean(HttpContext.Session.GetString(SessionKeyIsGameOn));
        int currentQuestionIndex = HttpContext.Session.GetInt32(SessionKeyCurrentQuestionIndex) ?? 0;
        int totalQuestions = HttpContext.Session.GetInt32(SessionKeyTotalQuestions) ?? 0;

        if (StartCommands.Contains(body) && !isGameOn)
        {
            await StartGame();
            HttpContext.Session.SetString(SessionKeyIsGameOn, "true");
            response.Message(PresentQuestionWithOptions(currentQuestionIndex));
            return TwiML(response);
        }

        if (OptionValues.Contains(body) && isGameOn)
        {
            var result = ProcessUserAnswer(body, currentQuestionIndex);
            response.Message(result);

            currentQuestionIndex++;

            if (currentQuestionIndex <= totalQuestions - 1)
            {
                HttpContext.Session.SetInt32(SessionKeyCurrentQuestionIndex, currentQuestionIndex);
                response.Append(new Message(PresentQuestionWithOptions(currentQuestionIndex)));
            }
            else
            {
                response.Append(new Message(EndTrivia()));
            }

            return TwiML(response);
        }

        response.Message(!isGameOn ? "*Hello! Send 'Start' or 'S' to play game*" : "*Invalid Input! Send a correct option 'A', 'B', 'C' or 'D'*");

        return TwiML(response);
    }

    private async Task StartGame()
    {
        if (HttpContext.Session.GetString(SessionKeyQuestions) != null)
        {
            HttpContext.Session.Remove(SessionKeyQuestions);
        }

        var trivia = await this.triviaService.GetTrivia();
        var questions = this.triviaService.ConvertTriviaToQuestions(trivia);

        AddNewQuestionsToSession(questions);
        HttpContext.Session.SetInt32(SessionKeyTotalQuestions, questions.Count);
    }

    private string ProcessUserAnswer(string userAnswer, int questionIndex)
    {
        bool optionIsCorrect = false;
        int score = HttpContext.Session.GetInt32(SessionKeyScore) ?? 0;
        var question = RetrieveQuestionFromSession(questionIndex);

        switch (userAnswer)
        {
            case "A":
                optionIsCorrect = question.Options[0].isCorrect;
                break;
            case "B":
                optionIsCorrect = question.Options[1].isCorrect;
                break;
            case "C":
                optionIsCorrect = question.Options[2].isCorrect;
                break;
            case "D":
                optionIsCorrect = question.Options[3].isCorrect;
                break;
        }

        if (optionIsCorrect)
        {
            score++;
            HttpContext.Session.SetInt32(SessionKeyScore, score);
        }

        return optionIsCorrect 
            ? "_Correct ✅_" 
            : $"_Incorrect ❌ Correct answer is {question.Options.Find(o => o.isCorrect).option.TrimEnd()}_";
    }

    private string PresentQuestionWithOptions(int questionIndex)
    {
        var question = RetrieveQuestionFromSession(questionIndex);
        return $"""
                {questionIndex + 1}. {question.QuestionText}
                {OptionValues[0]}. {question.Options[0].option}
                {OptionValues[1]}. {question.Options[1].option}
                {OptionValues[2]}. {question.Options[2].option}
                {OptionValues[3]}. {question.Options[3].option}
                """;
    }

    private void AddNewQuestionsToSession(List<Question> questions) =>
        HttpContext.Session.SetString(SessionKeyQuestions, JsonConvert.SerializeObject(questions));

    private Question RetrieveQuestionFromSession(int questionIndex)
    {
        var questionsFromSession = HttpContext.Session.GetString(SessionKeyQuestions);
        return JsonConvert.DeserializeObject<List<Question>>(questionsFromSession)[questionIndex];
    }

    private string EndTrivia()
    {
        var score = HttpContext.Session.GetInt32(SessionKeyScore) ?? 0;
        var totalQuestions = HttpContext.Session.GetInt32(SessionKeyTotalQuestions) ?? 0;

        var userResult = $"""
                Thanks for playing! 😊
                You answered {score} out of {totalQuestions} questions correctly.
                
                To play again, send 'Start' or 'S'
                """;

        HttpContext.Session.Clear();
        return userResult;
    }
}

This controller class is responsible for handling incoming messages, managing session state, and generating responses. It inherits from the TwilioController class provided by the Twilio.AspNet.Core library, which gives you access to the TwiML method. You can use this method to respond with TwiML, which is the Twilio Markup Language. The TriviaController class uses HttpContext.Sessionmethods to interact with the session.

The valid inputs are elements in theStartCommands and OptionValues read-only arrays. The body of the incoming message is compared with these elements to ensure that the user sent a proper input, if not, a message will be sent to the user prompting them to make the right input based on the game's current state. Other fields with the “SessionKey” prefix are used to define private constant strings for session keys in the program.


TheIndex method is the main action method that handles incoming HTTP POST requests from WhatsApp via the /Trivia route. It loads the session data using HttpContext.Session.LoadAsync() and retrieves data regarding the game state from the session using the HttpContext.Session.GetString() and HttpContext.Session.GetInt32() methods.

The use of underscores (_) and asterisks (*) at the beginning and end of certain strings is to achieve italic and bold text formatting respectively in rendered WhatsApp messages.

Each helper method in the TriviaController performs a specific task that supports the main functionality of the class.

  • StartGame method initializes the game by retrieving trivia questions, converting them into a format suitable for the game, and storing them in the session.
  • ProcessUserAnswer method processes the user's answer to a question and determines if it is correct or not.
  • PresentQuestionWithOptions method is responsible for formatting and presenting a question along with its options.
  • AddNewQuestionsToSession method stores a list of questions in the session. It converts the questions to JSON format and saves the JSON string in the session.
  • RetrieveQuestionFromSession method retrieves a question from the session using the question index.
  • EndTrivia method generates a message to end the trivia game. This method also removes session data related to the game. Based on the configuration of session service in Program.cs, this happens automatically when the session is idle for 40 seconds.

Test the Application

To test the application, you need to set up Twilio Sandbox for WhatsApp, make your application endpoint publicly accessible and add the endpoint URL in the Sandbox configuration as a webhook.

Setup Twilio Sandbox for WhatsApp

Go to the Twilio Console, navigate to Message > Try it out > Send a WhatsApp Message.

Follow the instructions on the page to connect to the sandbox, by sending a WhatsApp message from your device to the Twilio number provided in order to create a successful connection with the WhatsApp sandbox. Similarly, other individuals who wish to test your app with their respective numbers will need to follow the same procedure.

Expose your webhook using ngrok for testing

Now, open a shell terminal and run the following command to start ngrok and expose your local ASP.NET Core app by replacing <localhost-url> with the full URL of your localhost you copied initially:

ngrok http <localhost-url>

ngrok will generate a public URL that forwards requests to your local ASP.NET app. Look for the forwarding URL labelled Forwarding in the ngrok terminal window and copy it.

Go back to the Twilio Try WhatsApp page, click on Sandbox Settings, and change the When a message comes in endpoint url with the forwarding URL generated by ngrok plus the /Trivia route and ensure the method is set to POST. Then click Save to save the new sandbox configuration.

Project Demo

Run your ASP.NET Core project using the following command:

dotnet run

Now, test your application by sending a message in the initial conversation with the Twilio Sandbox number.

Conclusion

By leveraging the power of the Twilio platform and WhatsApp, you have created an immersive trivia game experience for users to enjoy. You have also learned how to save and retrieve data from sessions.

There are several ways this project can be improved upon. You can further improve this project by adding timer, handling exceptions, allowing users to choose difficulty and applying the chosen difficulty as a query parameter via The Trivia API URL (e.g https://the-trivia-api.com/api/questions?difficulty=hard). Additionally, you can explore the possibility of creating a solution that allows users to fill out surveys via WhatsApp.


Written by zadok | Passionate .NET developer with a mechanical engineering background.
Published by HackerNoon on 2023/09/15