diff --git a/arts/BotSharp.gif b/arts/BotSharp.gif new file mode 100644 index 00000000..84cb0140 Binary files /dev/null and b/arts/BotSharp.gif differ diff --git a/arts/Icon.png b/arts/Icon.png index 335e43ad..cf98da0b 100644 Binary files a/arts/Icon.png and b/arts/Icon.png differ diff --git a/arts/Logo.png b/arts/Logo.png index 2a1eb907..cf98da0b 100644 Binary files a/arts/Logo.png and b/arts/Logo.png differ diff --git a/docs/architecture/assets/architecture.drawio b/docs/architecture/assets/architecture.drawio new file mode 100644 index 00000000..2d602264 --- /dev/null +++ b/docs/architecture/assets/architecture.drawio @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Infrastructure/BotSharp.Abstraction/Users/Models/Token.cs b/src/Infrastructure/BotSharp.Abstraction/Users/Models/Token.cs index 8f3d7f0d..98ca34c1 100644 --- a/src/Infrastructure/BotSharp.Abstraction/Users/Models/Token.cs +++ b/src/Infrastructure/BotSharp.Abstraction/Users/Models/Token.cs @@ -1,5 +1,3 @@ -using System.Text.Json.Serialization; - namespace BotSharp.Abstraction.Users.Models; public class Token diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs index ec2cd1a4..acea5752 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Controllers/TwilioVoiceController.cs @@ -1,21 +1,7 @@ -using BotSharp.Abstraction.Conversations; -using BotSharp.Plugin.Twilio.Settings; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; -using System.Collections.Generic; -using System; -using Twilio.AspNet.Common; -using Twilio.AspNet.Core; -using Twilio.TwiML; -using Twilio.TwiML.Voice; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using BotSharp.Abstraction.Routing.Settings; -using Twilio.Http; -using Twilio.TwiML.Messaging; -using BotSharp.Abstraction.Agents.Enums; -using BotSharp.Abstraction.Conversations.Models; -using BotSharp.Abstraction.Conversations.Enums; +using System.IdentityModel.Tokens.Jwt; +using BotSharp.Plugin.Twilio.Services; namespace BotSharp.Plugin.Twilio.Controllers; @@ -30,12 +16,29 @@ public class TwilioVoiceController : TwilioController _settings = settings; _services = services; } - + + [Authorize] + [HttpGet("/twilio/token")] + public Token GetAccessToken() + { + var twilio = _services.GetRequiredService(); + var accessToken = twilio.GetAccessToken(); + var jwt = new JwtSecurityTokenHandler().ReadJwtToken(accessToken); + return new Token + { + AccessToken = accessToken, + ExpireTime = jwt.Payload.Exp.Value, + TokenType = "Bearer", + Scope = "api" + }; + } + [HttpPost("/twilio/voice/welcome")] public async Task StartConversation(VoiceRequest request) { string sessionId = $"TwilioVoice_{request.CallSid}"; - var response = ReturnInstructions("Hello, how may I help you?"); + var twilio = _services.GetRequiredService(); + var response = twilio.ReturnInstructions("Hello, how may I help you?"); return TwiML(response); } @@ -51,13 +54,18 @@ public class TwilioVoiceController : TwilioController $"calling_phone={input.DialCallSid}" }); + var twilio = _services.GetRequiredService(); VoiceResponse response = default; var result = await conv.SendMessage(agentId, new RoleDialogModel(AgentRole.User, input.SpeechResult), async msg => { - response = HangUp(msg.Content); + response = twilio.ReturnInstructions(msg.Content); + if (msg.FunctionName == "conversation_end") + { + response = twilio.HangUp(msg.Content); + } }, async functionExecuting => { }, async functionExecuted => @@ -66,53 +74,4 @@ public class TwilioVoiceController : TwilioController return TwiML(response); } - - private VoiceResponse ReturnInstructions(string message) - { - var routingSetting = _services.GetRequiredService(); - - var response = new VoiceResponse(); - var gather = new Gather() - { - Input = new List() - { - Gather.InputEnum.Speech - }, - Action = new Uri($"{_settings.CallbackHost}/twilio/voice/{routingSetting.RouterId}") - }; - gather.Say(message); - response.Append(gather); - return response; - } - - private VoiceResponse HangUp(string message) - { - var response = new VoiceResponse(); - if (!string.IsNullOrEmpty(message)) - { - response.Say(message); - } - response.Hangup(); - return response; - } - - private VoiceResponse HoldOn(int interval, string message = null) - { - var routingSetting = _services.GetRequiredService(); - - var response = new VoiceResponse(); - var gather = new Gather() - { - Input = new List() { Gather.InputEnum.Speech }, - Action = new Uri($"{_settings.CallbackHost}/twilio/voice/{routingSetting.RouterId}"), - ActionOnEmptyResult = true - }; - if (!string.IsNullOrEmpty(message)) - { - gather.Say(message); - } - gather.Pause(interval); - response.Append(gather); - return response; - } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs new file mode 100644 index 00000000..dcbcf30c --- /dev/null +++ b/src/Plugins/BotSharp.Plugin.Twilio/Services/TwilioService.cs @@ -0,0 +1,96 @@ +using Twilio.Jwt.AccessToken; +using Token = Twilio.Jwt.AccessToken.Token; + +namespace BotSharp.Plugin.Twilio.Services; + +/// +/// https://github.com/TwilioDevEd/voice-javascript-sdk-quickstart-csharp +/// +public class TwilioService +{ + private readonly TwilioSetting _settings; + private readonly IServiceProvider _services; + + public TwilioService(TwilioSetting settings, IServiceProvider services) + { + _settings = settings; + _services = services; + } + + public string GetAccessToken() + { + // These are specific to Voice + var user = _services.GetRequiredService(); + + // Create a Voice grant for this token + var grant = new VoiceGrant(); + grant.OutgoingApplicationSid = _settings.AppSID; + + // Optional: add to allow incoming calls + grant.IncomingAllow = true; + + var grants = new HashSet + { + { grant } + }; + + // Create an Access Token generator + var token = new Token( + _settings.AccountSID, + _settings.ApiKeySID, + _settings.ApiSecret, + user.Id, + grants: grants); + + return token.ToJwt(); + } + + public VoiceResponse ReturnInstructions(string message) + { + var routingSetting = _services.GetRequiredService(); + + var response = new VoiceResponse(); + var gather = new Gather() + { + Input = new List() + { + Gather.InputEnum.Speech + }, + Action = new Uri($"{_settings.CallbackHost}/twilio/voice/{routingSetting.RouterId}") + }; + gather.Say(message); + response.Append(gather); + return response; + } + + public VoiceResponse HangUp(string message) + { + var response = new VoiceResponse(); + if (!string.IsNullOrEmpty(message)) + { + response.Say(message); + } + response.Hangup(); + return response; + } + + public VoiceResponse HoldOn(int interval, string message = null) + { + var routingSetting = _services.GetRequiredService(); + + var response = new VoiceResponse(); + var gather = new Gather() + { + Input = new List() { Gather.InputEnum.Speech }, + Action = new Uri($"{_settings.CallbackHost}/twilio/voice/{routingSetting.RouterId}"), + ActionOnEmptyResult = true + }; + if (!string.IsNullOrEmpty(message)) + { + gather.Say(message); + } + gather.Pause(interval); + response.Append(gather); + return response; + } +} diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Settings/TwilioSetting.cs b/src/Plugins/BotSharp.Plugin.Twilio/Settings/TwilioSetting.cs index a1048b8c..3da775cf 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Settings/TwilioSetting.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Settings/TwilioSetting.cs @@ -5,5 +5,8 @@ public class TwilioSetting public string PhoneNumber { get; set; } public string AccountSID { get; set; } public string AuthToken { get; set; } + public string AppSID { get; set; } + public string ApiKeySID { get; set; } + public string ApiSecret { get; set; } public string CallbackHost { get; set; } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/TwilioPlugin.cs b/src/Plugins/BotSharp.Plugin.Twilio/TwilioPlugin.cs index 80b2b5df..cba9ba30 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/TwilioPlugin.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/TwilioPlugin.cs @@ -1,4 +1,4 @@ -using BotSharp.Plugin.Twilio.Settings; +using BotSharp.Plugin.Twilio.Services; namespace BotSharp.Plugin.Twilio; @@ -12,5 +12,7 @@ public class TwilioPlugin : IBotSharpPlugin var setting = new TwilioSetting(); config.Bind("Twilio", setting); services.AddSingleton(setting); + + services.AddScoped(); } } diff --git a/src/Plugins/BotSharp.Plugin.Twilio/Using.cs b/src/Plugins/BotSharp.Plugin.Twilio/Using.cs index 5026d491..66698a49 100644 --- a/src/Plugins/BotSharp.Plugin.Twilio/Using.cs +++ b/src/Plugins/BotSharp.Plugin.Twilio/Using.cs @@ -14,4 +14,13 @@ global using BotSharp.Abstraction.Models; global using BotSharp.Abstraction.Agents; global using BotSharp.Abstraction.Conversations; global using BotSharp.Abstraction.Plugins; -global using BotSharp.Abstraction.Conversations.Models; \ No newline at end of file +global using BotSharp.Abstraction.Conversations.Models; +global using BotSharp.Abstraction.Routing.Settings; +global using BotSharp.Abstraction.Conversations.Enums; +global using BotSharp.Abstraction.Users.Models; +global using BotSharp.Abstraction.Users; +global using Twilio.AspNet.Common; +global using Twilio.AspNet.Core; +global using Twilio.TwiML; +global using Twilio.TwiML.Voice; +global using BotSharp.Plugin.Twilio.Settings; \ No newline at end of file