paint-brush
Twilio と ASP.NET Core を使用してトリビア クイズ WhatsApp ボットを構築する@zadok
2,606 測定値
2,606 測定値

Twilio と ASP.NET Core を使用してトリビア クイズ WhatsApp ボットを構築する

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

長すぎる; 読むには

このチュートリアル ガイドでは、WhatsApp、ASP.NET Core、および The Trivia API 用の Twilio を使用してトリビア クイズを作成する方法を学習します。
featured image - Twilio と ASP.NET Core を使用してトリビア クイズ WhatsApp ボットを構築する
Zadok J. HackerNoon profile picture
0-item
1-item

トリビア ゲームは、新しい事実を学び、さまざまな主題にわたって知識を広げることができる、魅力的で教育的な体験を提供します。現在、トリビア クイズのモバイル アプリケーションや Web アプリケーションは、このようなアクティビティの最も一般的な分野です。 WhatsApp でトリビア ゲームをプレイしてみてはいかがですか?


このチュートリアル ガイドでは、WhatsApp、ASP.NET Core、および Twilio を使用してトリビア クイズ アプリケーションを作成する方法を学習します。トリビア API ( CC BY-NC 4.0に基づいてライセンス供与されています)。このチュートリアルの目的は、ユーザーが WhatsApp を使用して多肢選択式の質問をプレイして回答できるトリビア ゲーム アプリケーションを作成することです。活用しますASP.NET Core のセッションユーザーの進行状況を保存および取得し、スコアを追跡し、ゲームの状態を維持します。


これらの質問を取得するには、REST API である Trivia API を使用します。これにより、開発者は多肢選択式のトリビア質問を提供することでクイズ アプリを簡単に構築できます。 Trivia API の詳細については、__ Trivia API ドキュメント__ を参照してください。


前提条件

このチュートリアルを完了するには、次のものが必要です。


このチュートリアルのソース コードは GitHub にあります。


新しい ASP.NET Core プロジェクトをセットアップする

まず、優先作業ディレクトリでシェル端末を使用して、次のコマンドを実行して新しい Web API プロジェクトを作成します。


 dotnet new webapi -n TwilioWhatsAppTriviaApp --no-openapi


上記のスニペットの 2 番目のコマンドは、指定された名前で OpenAPI (Swagger) サポートなしで新しい Web API プロジェクトを作成します。プロジェクトで Swagger を使用する場合は、上記のコマンドで--no-openapiを省略してください。


次のコマンドを実行して、プロジェクト ディレクトリに移動します。


 cd TwilioWhatsAppTriviaApp


をインストールしますASP.NET Core 用の Twilio ヘルパー ライブラリNuGet パッケージ:


 dotnet add package Twilio.AspNet.Core


このライブラリにより、ASP.NET Core アプリケーションでの Twilio Webhook と API の操作が簡素化されます。


好みの IDE を使用してプロジェクトを開きます。 Controllersフォルダーで、ボイラープレート テンプレート コントローラー ファイルWeatherForecastController.csを削除し、プロジェクト ディレクトリ内のWeatherForcast.csも削除します。


次のコマンドを使用して、プロジェクトをビルドして実行し、これまでに行った作業がすべて正常に機能することを確認します。


 dotnet build dotnet run


プロジェクトが正常に実行されたら、デバッグ コンソールに表示されるローカルホスト URL をメモします。これらの URL のいずれかを使用して、ngrok を使用してパブリックにアクセス可能なローカル Web サーバーをセットアップできます。


ローカルホストの URL


セッションの実装

セッションは、ASP.NET Core アプリケーションにユーザーのデータを保存するいくつかの方法の 1 つです。これは、デフォルトでは 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を作成します。次のコード サンプルに示すようなプロパティを持つ 2 つのモデル クラス ファイル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クラスが役に立ちます。このクラスは、質問のテキストや選択肢のリストなど、トリビアの質問に必要な情報をカプセル化します。各オプションは、オプション テキストと、それが正しいオプションかどうかを示すブール値を含むタプルで表されます。


Trivia サービス クラスを追加する

プロジェクト ディレクトリに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クラスには、 GetTriviaConvertTriviaToQuestionsという 2 つのメソッドが含まれています。 GetTriviaメソッドは、3 つの質問のみを返すことを指定するクエリ パラメーターlimit=3指定して、HTTP GET リクエストを Trivia API に送信します。制限パラメータを指定しない場合、API はデフォルトで 10 個の質問を返します。


ConvertTriviaToQuestionsメソッドは、API からの応答を体系的な方法に変換します。また、このメソッドはすべての質問の選択肢をランダムにシャッフルするので、1 つの選択肢がすべての質問の答えになるわけではありません。


TriviaServiceと HTTP クライアントをアプリケーションの依存関係挿入 (DI) コンテナーに登録するには、次のコードに示すように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メソッドを使用してセッションと対話します。

有効な入力は、 StartCommandsおよびOptionValues読み取り専用配列の要素です。受信メッセージの本文がこれらの要素と比較され、ユーザーが適切な入力を送信したかどうかが確認されます。送信されていない場合は、ゲームの現在の状態に基づいて正しい入力を行うよう求めるメッセージがユーザーに送信されます。 「SessionKey」プレフィックスが付いた他のフィールドは、プログラム内のセッション キーのプライベート定数文字列を定義するために使用されます。


Indexメソッドは、WhatsApp から/Triviaルート経由で受信する 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 サンドボックスを設定し、アプリケーション エンドポイントをパブリックにアクセスできるようにし、サンドボックス構成にエンドポイント URL を Webhook として追加する必要があります。

WhatsApp 用の Twilio サンドボックスをセットアップする

に行きますTwilio コンソール, [メッセージ] > [試してみる] > [WhatsApp メッセージを送信]に移動します。


Twilio コンソール


ページの指示に従ってサンドボックスに接続します。WhatsApp サンドボックスとの接続を成功させるために、デバイスから指定された Twilio 番号に WhatsApp メッセージを送信します。同様に、それぞれの番号を使用してアプリをテストしたい他の人も、同じ手順に従う必要があります。

テスト用に ngrok を使用して Webhook を公開する

次に、シェル ターミナルを開き、次のコマンドを実行して ngrok を起動し、 <localhost-url>を最初にコピーしたローカルホストの完全な URL に置き換えて、ローカル ASP.NET Core アプリを公開します。


 ngrok http <localhost-url>


ngrok は、リクエストをローカル ASP.NET アプリに転送するパブリック URL を生成します。 ngrok ターミナル ウィンドウでForwardingというラベルの付いた転送 URL を探してコピーします。


転送先URL


Twilio の Try WhatsApp ページに戻り、 [サンドボックス設定]をクリックし、エンドポイント URLにメッセージが届いたときを、 ngrok によって生成された転送URL に/Triviaルートを加えたものに変更し、メソッドが POST に設定されていることを確認します。次に、「保存」をクリックして新しいサンドボックス構成を保存します。


Twilio サンドボックス


プロジェクトのデモ

次のコマンドを使用して、ASP.NET Core プロジェクトを実行します。


 dotnet run


ここで、最初の会話で Twilio サンドボックス番号とのメッセージを送信して、アプリケーションをテストします。


WhatsApp でアプリケーションをテストする


結論

Twilio プラットフォームと WhatsApp の力を活用することで、ユーザーが楽しめる没入型のトリビア ゲーム エクスペリエンスを作成しました。また、セッションからデータを保存および取得する方法も学習しました。


このプロジェクトを改善する方法はいくつかあります。タイマーを追加し、例外を処理し、ユーザーが難易度を選択できるようにし、選択した難易度を Trivia API URL 経由でクエリ パラメーターとして適用することで、このプロジェクトをさらに改善できます (例: https://the-trivia-api.com/api/questions?difficulty=hard )。さらに、ユーザーが WhatsApp 経由でアンケートに回答できるソリューションを作成する可能性を検討することもできます。