トリビア ゲームは、新しい事実を学び、さまざまな主題にわたって知識を広げることができる、魅力的で教育的な体験を提供します。現在、トリビア クイズのモバイル アプリケーションや Web アプリケーションは、このようなアクティビティの最も一般的な分野です。 WhatsApp でトリビア ゲームをプレイしてみてはいかがですか? このチュートリアル ガイドでは、WhatsApp、ASP.NET Core、および Twilio を使用してトリビア クイズ アプリケーションを作成する方法を学習します。 ( )。このチュートリアルの目的は、ユーザーが WhatsApp を使用して多肢選択式の質問をプレイして回答できるトリビア ゲーム アプリケーションを作成することです。活用します ユーザーの進行状況を保存および取得し、スコアを追跡し、ゲームの状態を維持します。 トリビア API CC BY-NC 4.0に基づいてライセンス供与されています ASP.NET Core のセッション これらの質問を取得するには、REST API である Trivia API を使用します。これにより、開発者は多肢選択式のトリビア質問を提供することでクイズ アプリを簡単に構築できます。 Trivia API の詳細については、__ __ を参照してください。 Trivia API ドキュメント 前提条件 このチュートリアルを完了するには、次のものが必要です。 .NETをサポートするOS(Windows/macOS/Linux) .NET 7 SDK コード エディターまたは IDE (推奨: 、 とともに 、 または ) ビジュアルスタジオ Visual Studioコード C# プラグイン ジェットブレインズライダー ngrok CLI 無料または有料の Twilio アカウント (お持ちでない場合は、 ) Twilio を無料で試してみる C# および ASP.NET Core の経験 。 このチュートリアルのソース コードは GitHub にあります。 新しい ASP.NET Core プロジェクトをセットアップする まず、優先作業ディレクトリでシェル端末を使用して、次のコマンドを実行して新しい Web API プロジェクトを作成します。 dotnet new webapi -n TwilioWhatsAppTriviaApp --no-openapi 上記のスニペットの 2 番目のコマンドは、指定された名前で OpenAPI (Swagger) サポートなしで新しい Web API プロジェクトを作成します。プロジェクトで Swagger を使用する場合は、上記のコマンドで を省略してください。 --no-openapi 次のコマンドを実行して、プロジェクト ディレクトリに移動します。 cd TwilioWhatsAppTriviaApp をインストールします NuGet パッケージ: ASP.NET Core 用の Twilio ヘルパー ライブラリ 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 サーバーをセットアップできます。 セッションの実装 セッションは、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() はセッション サービスを登録し、アプリケーションがセッション状態を維持できるようにします。 パラメーターを使用すると、さまざまなセッション関連のオプションを構成できます。 、セッションがアイドル状態とみなされるまでの非アクティブ期間を設定するために使用されます。この場合は40秒に設定されています。 Cookie の拒否が有効になっているシナリオでも、セッションの状態が機能し続けることを保証します。 AddSession() options IdleTimeout Cookie.IsEssential セッション サポートは、 ミドルウェアをアプリケーション パイプラインに追加することで有効になります。つまり、アプリケーションは、データの保存と取得に使用できるセッション オブジェクトにアクセスできるようになります。 UseSession モデルを作成する プロジェクト ディレクトリに新しいフォルダー を作成します。次のコード サンプルに示すようなプロパティを持つ 2 つのモデル クラス ファイル と を追加します。 Models TriviaApiResponse.cs Question.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; } } モデルには、Trivia API 応答のフィールドを表すプロパティが含まれています。 属性により、各プロパティに対応する JSON データが正しく設定されるようになります。 TriviaApiResponse JsonProperty トリビアの質問を効率的に処理するには、 クラスが役に立ちます。このクラスは、質問のテキストや選択肢のリストなど、トリビアの質問に必要な情報をカプセル化します。各オプションは、オプション テキストと、それが正しいオプションかどうかを示すブール値を含むタプルで表されます。 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; } } クラスには、 と という 2 つのメソッドが含まれています。 メソッドは、3 つの質問のみを返すことを指定するクエリ パラメーター 指定して、HTTP GET リクエストを Trivia API に送信します。制限パラメータを指定しない場合、API はデフォルトで 10 個の質問を返します。 TriviaService GetTrivia ConvertTriviaToQuestions GetTrivia limit=3 メソッドは、API からの応答を体系的な方法に変換します。また、このメソッドはすべての質問の選択肢をランダムにシャッフルするので、1 つの選択肢がすべての質問の答えになるわけではありません。 ConvertTriviaToQuestions と HTTP クライアントをアプリケーションの依存関係挿入 (DI) コンテナーに登録するには、次のコードに示すように を変更します。 TriviaService 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 コントローラーを作成する という名前のファイル内の空の API コントローラー クラスを フォルダーに追加し、次のコードに示すようにその内容を変更します。 TriviaController.cs 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 ライブラリによって提供される クラスを継承しており、 メソッドへのアクセスを提供します。このメソッドを使用して次のように応答できます。 。 クラスは、 メソッドを使用してセッションと対話します。 有効な入力は、 および 読み取り専用配列の要素です。受信メッセージの本文がこれらの要素と比較され、ユーザーが適切な入力を送信したかどうかが確認されます。送信されていない場合は、ゲームの現在の状態に基づいて正しい入力を行うよう求めるメッセージがユーザーに送信されます。 「SessionKey」プレフィックスが付いた他のフィールドは、プログラム内のセッション キーのプライベート定数文字列を定義するために使用されます。 TwilioController TwiML TwiML (Twilio マークアップ言語) TriviaController HttpContext.Session StartCommands OptionValues メソッドは、WhatsApp から ルート経由で受信する HTTP POST リクエストを処理するメインのアクション メソッドです。 を使用してセッション データを読み込み、 メソッドと メソッドを使用してセッションからゲーム状態に関するデータを取得します。 Index /Trivia HttpContext.Session.LoadAsync() HttpContext.Session.GetString() HttpContext.Session.GetInt32() 特定の文字列の先頭と末尾にアンダースコア (_) とアスタリスク (*) を使用すると、表示される WhatsApp メッセージでそれぞれ斜体と太字のテキスト形式が実現されます。 の各ヘルパー メソッドは、クラスの主な機能をサポートする特定のタスクを実行します。 TriviaController メソッドは、トリビアの質問を取得し、ゲームに適した形式に変換し、セッションに保存することでゲームを初期化します。 StartGame メソッドは、質問に対するユーザーの回答を処理し、それが正しいかどうかを判断します。 ProcessUserAnswer メソッドは、質問をそのオプションとともにフォーマットして提示する役割を果たします。 PresentQuestionWithOptions メソッドは、セッション内の質問のリストを保存します。質問を JSON 形式に変換し、JSON 文字列をセッションに保存します。 AddNewQuestionsToSession メソッドは、質問インデックスを使用してセッションから質問を取得します。 RetrieveQuestionFromSession メソッドは、トリビア ゲームを終了するメッセージを生成します。このメソッドは、ゲームに関連するセッション データも削除します。 のセッション サービスの構成に基づいて、これはセッションが 40 秒間アイドル状態になると自動的に行われます。 EndTrivia Program.cs アプリケーションをテストする アプリケーションをテストするには、WhatsApp 用に Twilio サンドボックスを設定し、アプリケーション エンドポイントをパブリックにアクセスできるようにし、サンドボックス構成にエンドポイント URL を Webhook として追加する必要があります。 WhatsApp 用の Twilio サンドボックスをセットアップする に行きます , に移動します。 Twilio コンソール [メッセージ] > [試してみる] > [WhatsApp メッセージを送信] ページの指示に従ってサンドボックスに接続します。WhatsApp サンドボックスとの接続を成功させるために、デバイスから指定された Twilio 番号に WhatsApp メッセージを送信します。同様に、それぞれの番号を使用してアプリをテストしたい他の人も、同じ手順に従う必要があります。 テスト用に ngrok を使用して Webhook を公開する 次に、シェル ターミナルを開き、次のコマンドを実行して ngrok を起動し、 を最初にコピーしたローカルホストの完全な URL に置き換えて、ローカル ASP.NET Core アプリを公開します。 <localhost-url> ngrok http <localhost-url> ngrok は、リクエストをローカル ASP.NET アプリに転送するパブリック URL を生成します。 ngrok ターミナル ウィンドウで というラベルの付いた転送 URL を探してコピーします。 Forwarding Twilio の Try WhatsApp ページに戻り、 をクリックし、エンドポイント URL ngrok によって生成された URL に ルートを加えたものに変更し、メソッドが POST に設定されていることを確認します。次に、「保存」をクリックして新しいサンドボックス構成を保存します。 [サンドボックス設定] にメッセージが届いたときを、 転送 /Trivia プロジェクトのデモ 次のコマンドを使用して、ASP.NET Core プロジェクトを実行します。 dotnet run ここで、最初の会話で Twilio サンドボックス番号とのメッセージを送信して、アプリケーションをテストします。 結論 Twilio プラットフォームと WhatsApp の力を活用することで、ユーザーが楽しめる没入型のトリビア ゲーム エクスペリエンスを作成しました。また、セッションからデータを保存および取得する方法も学習しました。 このプロジェクトを改善する方法はいくつかあります。タイマーを追加し、例外を処理し、ユーザーが難易度を選択できるようにし、選択した難易度を Trivia API URL 経由でクエリ パラメーターとして適用することで、このプロジェクトをさらに改善できます (例: )。さらに、ユーザーが WhatsApp 経由でアンケートに回答できるソリューションを作成する可能性を検討することもできます。 https://the-trivia-api.com/api/questions?difficulty=hard