paint-brush
Twilio 및 ASP.NET Core를 사용하여 상식 퀴즈 WhatsApp Bot 구축~에 의해@zadok
2,606 판독값
2,606 판독값

Twilio 및 ASP.NET Core를 사용하여 상식 퀴즈 WhatsApp Bot 구축

~에 의해 Zadok J.15m2023/09/15
Read on Terminal Reader
Read this story w/o Javascript

너무 오래; 읽다

이 자습서 가이드에서는 WhatsApp, ASP.NET Core 및 Trivia API용 Twilio를 사용하여 퀴즈를 만드는 방법을 알아봅니다.
featured image - Twilio 및 ASP.NET Core를 사용하여 상식 퀴즈 WhatsApp Bot 구축
Zadok J. HackerNoon profile picture
0-item
1-item

퀴즈 게임은 새로운 사실을 배우고 다양한 주제에 걸쳐 지식을 확장할 수 있는 매력적이고 교육적인 경험을 제공합니다. 요즘에는 퀴즈 모바일 및 웹 애플리케이션이 이러한 활동을 위한 가장 일반적인 이동 영역입니다. WhatsApp에서 상식 게임을 플레이해 보는 것은 어떨까요?


이 자습서 가이드에서는 WhatsApp, ASP.NET Core 및 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 프로젝트 설정

시작하려면 기본 작업 디렉터리에서 셸 터미널을 사용하여 다음 명령을 실행하여 새 웹 API 프로젝트를 만듭니다.


 dotnet new webapi -n TwilioWhatsAppTriviaApp --no-openapi


위 코드 조각의 두 번째 명령은 지정된 이름을 사용하고 OpenAPI(Swagger) 지원 없이 새 웹 API 프로젝트를 생성합니다. 프로젝트에서 Swagger를 사용하려면 위 명령에서 --no-openapi 생략하면 됩니다.


다음 명령을 실행하여 프로젝트 디렉터리로 변경합니다.


 cd TwilioWhatsAppTriviaApp


설치하다 ASP.NET Core용 Twilio 도우미 라이브러리 NuGet 패키지:


 dotnet add package Twilio.AspNet.Core


이 라이브러리는 ASP.NET Core 애플리케이션에서 Twilio 웹후크 및 API 작업을 단순화합니다.


원하는 IDE를 사용하여 프로젝트를 엽니다. Controllers 폴더에서 상용구 템플릿 컨트롤러 파일 WeatherForecastController.cs 를 제거하고 프로젝트 디렉터리에서 WeatherForcast.cs 도 제거합니다.


다음 명령을 사용하여 지금까지 수행한 모든 작업이 제대로 작동하는지 확인하려면 프로젝트를 빌드하고 실행하세요.


 dotnet build dotnet run


프로젝트를 성공적으로 실행한 후 디버깅 콘솔에 나타나는 localhost URL을 기록해 두십시오. 이러한 URL 중 하나를 사용하여 ngrok를 통해 공개적으로 액세스 가능한 로컬 웹 서버를 설정할 수 있습니다.


로컬호스트 URL


세션 구현

세션은 ASP.NET Core 애플리케이션에서 사용자 데이터를 저장하는 여러 방법 중 하나입니다. 기본적으로 HTTP 프로토콜은 상태 비저장(stateless)이므로 요청 간에 사용자 데이터를 유지하려는 경우 이는 필수적입니다. 즉, 데이터가 보존되지 않음을 의미합니다.

다음 코드에 표시된 대로 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 쿠키 거부가 활성화된 시나리오에서도 세션 상태가 계속 작동하도록 보장합니다.


애플리케이션 파이프라인에 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 클래스에는 GetTriviaConvertTriviaToQuestions 라는 두 가지 메서드가 포함되어 있습니다. GetTrivia 메서드는 3개의 질문만 반환되도록 지정하는 쿼리 매개 변수 limit=3 사용하여 Trivia API에 HTTP GET 요청을 보냅니다. 제한 매개변수가 없으면 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();


퀴즈 컨트롤러 만들기

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


이 컨트롤러 클래스는 들어오는 메시지 처리, 세션 상태 관리 및 응답 생성을 담당합니다. 이는 TwiML 메서드에 대한 액세스를 제공하는 Twilio.AspNet.Core 라이브러리에서 제공하는 TwilioController 클래스에서 상속됩니다. 이 방법을 사용하여 다음과 같이 응답할 수 있습니다. Twilio 마크업 언어인 TwiML . 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을 웹훅으로 추가해야 합니다.

WhatsApp용 Twilio Sandbox 설정

로 이동 Twilio 콘솔 에서 메시지 > 사용해 보기 > WhatsApp 메시지 보내기 로 이동하세요.


Twilio 콘솔


WhatsApp 샌드박스와의 성공적인 연결을 생성하기 위해 장치에서 제공된 Twilio 번호로 WhatsApp 메시지를 보내 페이지의 지침에 따라 샌드박스에 연결합니다. 마찬가지로, 각자의 번호로 앱을 테스트하려는 다른 개인도 동일한 절차를 따라야 합니다.

테스트를 위해 ngrok를 사용하여 웹훅을 노출합니다.

이제 셸 터미널을 열고 다음 명령을 실행하여 ngrok를 시작하고 <localhost-url> 처음에 복사한 로컬 호스트의 전체 URL로 바꿔 로컬 ASP.NET Core 앱을 노출합니다.


 ngrok http <localhost-url>


ngrok는 요청을 로컬 ASP.NET 앱으로 전달하는 공용 URL을 생성합니다. ngrok 터미널 창에서 Forwarding이라고 표시된 전달 URL을 찾아 복사하세요.


전달 URL


Twilio Try WhatsApp 페이지로 돌아가서 Sandbox 설정을 클릭한 다음 ngrok에서 생성된 전달 URL과 /Trivia 경로를 사용하여 메시지가 엔드포인트에 들어올 때 URL을 변경하고 메서드가 POST로 설정되어 있는지 확인하세요. 그런 다음 저장을 클릭하여 새 샌드박스 구성을 저장합니다.


트윌리오 샌드박스


프로젝트 데모

다음 명령을 사용하여 ASP.NET Core 프로젝트를 실행합니다.


 dotnet run


이제 Twilio Sandbox 번호와의 초기 대화에서 메시지를 보내 애플리케이션을 테스트해 보세요.


WhatsApp에서 애플리케이션 테스트


결론

Twilio 플랫폼과 WhatsApp의 강력한 기능을 활용하여 사용자가 즐길 수 있는 몰입형 퀴즈 게임 환경을 만들었습니다. 또한 세션에서 데이터를 저장하고 검색하는 방법도 배웠습니다.


이 프로젝트를 개선할 수 있는 방법에는 여러 가지가 있습니다. 타이머를 추가하고, 예외를 처리하고, 사용자가 난이도를 선택할 수 있도록 허용하고, Trivia API URL을 통해 선택한 난이도를 쿼리 매개변수로 적용하여 이 프로젝트를 더욱 개선할 수 있습니다(예: https://the-trivia-api.com/api/questions?difficulty=hard ). 또한 사용자가 WhatsApp을 통해 설문조사를 작성할 수 있는 솔루션을 만들 가능성도 탐색할 수 있습니다.