paint-brush
使用 Twilio 和 ASP.NET Core 构建问答测验 WhatsApp 机器人经过@zadok
2,844 讀數
2,844 讀數

使用 Twilio 和 ASP.NET Core 构建问答测验 WhatsApp 机器人

经过 Zadok J.15m2023/09/15
Read on Terminal Reader

太長; 讀書

在本教程指南中,您将学习如何使用 Twilio for WhatsApp、ASP.NET Core 和 Trivia API 创建琐事测验。
featured image - 使用 Twilio 和 ASP.NET Core 构建问答测验 WhatsApp 机器人
Zadok J. HackerNoon profile picture
0-item
1-item

问答游戏提供了一种引人入胜的教育体验,您可以在其中学习新的事实并扩展各个学科的知识。如今,问答游戏移动和网络应用程序是此类活动最常见的领域。在 WhatsApp 上玩一个问答游戏怎么样?


在本教程指南中,您将学习如何使用 Twilio for WhatsApp、ASP.NET Core 和琐事 API根据 CC BY-NC 4.0 许可)。本教程的目的是创建一个问答游戏应用程序,允许用户使用 WhatsApp 玩并回答多项选择题。你将利用ASP.NET Core 中的会话存储和检索用户进度、跟踪得分并维护游戏状态。


为了获取这些问题,您将使用 Trivia API(一种 REST API),它使开发人员可以通过提供多项选择问答题轻松构建测验应用程序。要了解有关 Trivia API 的更多信息,请访问__Trivia API 文档__。


先决条件

要完成本教程,您将需要:


本教程的源代码可以在 GitHub 上找到


设置新的 ASP.NET Core 项目

首先,在首选工作目录中使用 shell 终端,运行以下命令来创建新的 Web API 项目:


 dotnet new webapi -n TwilioWhatsAppTriviaApp --no-openapi


上面代码片段中的第二个命令将创建一个具有指定名称且不支持 OpenAPI (Swagger) 的新 Web API 项目。如果你想在项目中使用Swagger,只需在上面的命令中省略--no-openapi即可。


通过运行以下命令更改到项目目录:


 cd TwilioWhatsAppTriviaApp


安装适用于 ASP.NET Core 的 Twilio 帮助程序库NuGet 包:


 dotnet add package Twilio.AspNet.Core


该库简化了 ASP.NET Core 应用程序中 Twilio Webhooks 和 API 的使用。


使用您喜欢的 IDE 打开项目。在Controllers文件夹中,删除样板模板控制器文件WeatherForecastController.cs ,并删除项目目录中的WeatherForcast.cs


使用以下命令构建并运行您的项目,以确保您迄今为止所做的一切都能正常运行:


 dotnet build dotnet run


成功运行项目后,记下调试控制台中显示的任何本地主机 URL。您可以使用这些 URL 中的任何一个来使用 ngrok 设置可公开访问的本地 Web 服务器。


本地主机 URL


实施会议

会话是在 ASP.NET Core 应用程序中存储用户数据的几种方法之一。当您想要在请求之间保留用户数据时,这一点至关重要,因为默认情况下,HTTP 协议是无状态的 - 这意味着数据不会被保留。

通过修改Program.cs添加内存会话提供程序,如下代码所示:


 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()注册分布式内存缓存服务。该服务提供内存缓存,可用于跨多个请求或会话存储和检索数据。


AddSession()注册会话服务,使应用程序能够维护会话状态。 options参数允许您配置各种与会话相关的选项。 IdleTimeout用于设置不活动的持续时间,超过该时间后会话将被视为空闲。在本例中,它设置为 40 秒。 Cookie.IsEssential确保会话状态保持正常运行,即使在启用 cookie 拒绝的情况下也是如此。


通过将UseSession中间件添加到应用程序管道来启用会话支持,也就是说,您的应用程序可以获得对可用于存储和检索数据的会话对象的访问权限。


创建模型

在项目目录中创建一个新文件夹Models 。添加两个模型类文件TriviaApiResponse.csQuestion.cs ,其属性如以下代码示例所示:


 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; } }


TriviaApiResponse模型包含表示 Trivia API 响应字段的属性。 JsonProperty属性确保每个属性都正确填充相应的 JSON 数据。


为了以简化的方式处理琐碎问题, Question类可以提供帮助。此类封装了琐事问题的必要信息,包括问题文本和选项列表。每个选项都由一个元组表示,其中包含选项文本和一个指示其是否是正确选项的布尔值。


添加琐事服务类

在项目目录中创建一个Services文件夹,并添加一个名为TriviaService.cs的新类文件。修改其内容,如下代码所示:


 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; } }


TriviaService类包含两个方法: GetTriviaConvertTriviaToQuestionsGetTrivia方法将 HTTP GET 请求发送到 Trivia API,其中包含查询参数limit=3 ,该参数指定仅应返回 3 个问题。如果没有 limit 参数,API 默认返回 10 个问题。


ConvertTriviaToQuestions方法将 API 的响应转换为有组织的方式。该方法还会随机打乱所有问题选项,因此单个选项不会成为所有问题的答案。


要在应用程序的依赖注入 (DI) 容器中注册TriviaService和 HTTP 客户端,请修改Program.cs ,如以下代码所示:


 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();


创建 Trivia 控制器

将名为TriviaController.cs的文件中的空 API 控制器类添加到Controllers文件夹中,并修改其内容,如以下代码所示:


 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; } }


该控制器类负责处理传入消息、管理会话状态和生成响应。它继承自 Twilio.AspNet.Core 库提供的TwilioController类,该类使您可以访问TwiML方法。您可以使用此方法来响应TwiML,即 Twilio 标记语言TriviaController类使用HttpContext.Session方法与会话进行交互。

有效输入是StartCommandsOptionValues只读数组中的元素。将传入消息的正文与这些元素进行比较,以确保用户发送了正确的输入,如果没有,则会向用户发送一条消息,提示他们根据游戏的当前状态做出正确的输入。其他带有“SessionKey”前缀的字段用于定义程序中会话密钥的私有常量字符串。


Index方法是主要操作方法,用于处理通过/Trivia路由从 WhatsApp 传入的 HTTP POST 请求。它使用HttpContext.Session.LoadAsync()加载会话数据,并使用HttpContext.Session.GetString()HttpContext.Session.GetInt32()方法从会话中检索有关游戏状态的数据。


在某些字符串的开头和结尾使用下划线 (_) 和星号 (*) 是为了在呈现的 WhatsApp 消息中分别实现斜体和粗体文本格式。


TriviaController中的每个辅助方法都执行支持该类主要功能的特定任务。

  • StartGame方法通过检索琐事问题、将其转换为适合游戏的格式并将其存储在会话中来初始化游戏。
  • ProcessUserAnswer方法处理用户对问题的回答并确定其是否正确。
  • PresentQuestionWithOptions方法负责格式化和呈现问题及其选项。
  • AddNewQuestionsToSession方法在会话中存储问题列表。它将问题转换为 JSON 格式并将 JSON 字符串保存在会话中。
  • RetrieveQuestionFromSession方法使用问题索引从会话中检索问题。
  • EndTrivia方法生成一条消息来结束问答游戏。此方法还会删除与游戏相关的会话数据。根据Program.cs中会话服务的配置,当会话空闲 40 秒时,会自动发生这种情况。


测试应用程序

要测试应用程序,您需要为 WhatsApp 设置 Twilio Sandbox,使您的应用程序端点可公开访问,并将端点 URL 添加到 Sandbox 配置中作为 Webhook。

为 WhatsApp 设置 Twilio 沙盒

前往Twilio 控制台,导航至消息 > 试用 > 发送 WhatsApp 消息


Twilio 控制台


按照页面上的说明连接到沙箱,从您的设备向提供的 Twilio 号码发送 WhatsApp 消息,以便与 WhatsApp 沙箱创建成功连接。同样,其他想要使用各自号码测试您的应用程序的人也需要遵循相同的程序。

使用 ngrok 公开您的 webhook 进行测试

现在,打开 shell 终端并运行以下命令来启动 ngrok 并通过将<localhost-url>替换为您最初复制的 localhost 的完整 URL 来公开本地 ASP.NET Core 应用程序:


 ngrok http <localhost-url>


ngrok 将生成一个公共 URL,将请求转发到本地 ASP.NET 应用程序。在 ngrok 终端窗口中查找标记为Forwarding的转发 URL 并复制它。


转发网址


返回 Twilio Try WhatsApp 页面,单击Sandbox Settings ,然后使用 ngrok 生成的转发URL 和/Trivia路由更改当消息传入端点 url 时,并确保方法设置为 POST。然后单击“保存”以保存新的沙箱配置。


Twilio 沙箱


项目演示

使用以下命令运行 ASP.NET Core 项目:


 dotnet run


现在,通过在与 Twilio Sandbox 号码的初始对话中发送消息来测试您的应用程序。


在 WhatsApp 中测试您的应用程序


结论

通过利用 Twilio 平台和 WhatsApp 的强大功能,您为用户打造了身临其境的问答游戏体验。您还学习了如何从会话中保存和检索数据。


有多种方法可以改进该项目。您可以通过添加计时器、处理异常、允许用户选择难度以及通过 Trivia API URL 将所选难度应用为查询参数来进一步改进该项目(例如https://the-trivia-api.com/api/questions?difficulty=hard )。此外,您可以探索创建一个允许用户通过 WhatsApp 填写调查的解决方案的可能性。