From aeb5ff87664a985687df282b0c2ed97f326b354e Mon Sep 17 00:00:00 2001 From: Oceania2018 Date: Wed, 18 Jul 2018 20:56:17 -0500 Subject: [PATCH] added facebook messenger webhook --- BotSharp.Core/Agents/Agent.cs | 3 + BotSharp.Core/Agents/AgentIntegration.cs | 26 ++++ BotSharp.Core/Engines/AgentImportHeader.cs | 8 +- .../Dialogflow/AgentImporterInDialogflow.cs | 5 + .../BotSharp.MachineLearning.csproj | 4 + .../SpaCy/server.py | 0 .../Integrations/AmazonAlexaController.cs | 12 ++ .../FacebookMessengerController.cs | 117 ++++++++++++++++++ .../FacebookMessenger/IWebhookMessageBody.cs | 10 ++ .../FacebookMessenger/WebhookEvent.cs | 14 +++ .../FacebookMessenger/WebhookEventEntry.cs | 13 ++ .../FacebookMessenger/WebhookMessage.cs | 29 +++++ .../WebhookMessageQuickReply.cs | 11 ++ .../WebhookMessageRecipient.cs | 14 +++ .../FacebookMessenger/WebhookMessageSender.cs | 14 +++ .../FacebookMessenger/WebhookTextMessage.cs | 15 +++ .../Integrations/SkypeController.cs | 12 ++ .../Integrations/SlackController.cs | 12 ++ .../TelegramController.cs} | 8 +- .../Integrations/TwitterController.cs | 12 ++ .../Integrations/ViberController.cs | 12 ++ BotSharp.UnitTest/ConversationTest.cs | 5 +- .../App_Data/DbInitializer/Agents/agents.json | 5 +- .../wwwroot/images/spotify-clip.png | Bin 0 -> 7089 bytes BotSharp.WebHost/wwwroot/images/spotify.png | Bin 0 -> 5628 bytes 25 files changed, 349 insertions(+), 12 deletions(-) create mode 100644 BotSharp.Core/Agents/AgentIntegration.cs rename {BotSharp.Core/Engines => BotSharp.MachineLearning}/SpaCy/server.py (100%) create mode 100644 BotSharp.RestApi/Integrations/AmazonAlexaController.cs create mode 100644 BotSharp.RestApi/Integrations/FacebookMessenger/FacebookMessengerController.cs create mode 100644 BotSharp.RestApi/Integrations/FacebookMessenger/IWebhookMessageBody.cs create mode 100644 BotSharp.RestApi/Integrations/FacebookMessenger/WebhookEvent.cs create mode 100644 BotSharp.RestApi/Integrations/FacebookMessenger/WebhookEventEntry.cs create mode 100644 BotSharp.RestApi/Integrations/FacebookMessenger/WebhookMessage.cs create mode 100644 BotSharp.RestApi/Integrations/FacebookMessenger/WebhookMessageQuickReply.cs create mode 100644 BotSharp.RestApi/Integrations/FacebookMessenger/WebhookMessageRecipient.cs create mode 100644 BotSharp.RestApi/Integrations/FacebookMessenger/WebhookMessageSender.cs create mode 100644 BotSharp.RestApi/Integrations/FacebookMessenger/WebhookTextMessage.cs create mode 100644 BotSharp.RestApi/Integrations/SkypeController.cs create mode 100644 BotSharp.RestApi/Integrations/SlackController.cs rename BotSharp.RestApi/{FacebookMessengerController.cs => Integrations/TelegramController.cs} (50%) create mode 100644 BotSharp.RestApi/Integrations/TwitterController.cs create mode 100644 BotSharp.RestApi/Integrations/ViberController.cs create mode 100644 BotSharp.WebHost/wwwroot/images/spotify-clip.png create mode 100644 BotSharp.WebHost/wwwroot/images/spotify.png diff --git a/BotSharp.Core/Agents/Agent.cs b/BotSharp.Core/Agents/Agent.cs index e24ed2fb..dd67022a 100644 --- a/BotSharp.Core/Agents/Agent.cs +++ b/BotSharp.Core/Agents/Agent.cs @@ -72,5 +72,8 @@ namespace BotSharp.Core.Agents [ForeignKey("AgentId")] public AgentMlConfig MlConfig { get; set; } + + [ForeignKey("AgentId")] + public List Integrations { get; set; } } } diff --git a/BotSharp.Core/Agents/AgentIntegration.cs b/BotSharp.Core/Agents/AgentIntegration.cs new file mode 100644 index 00000000..aa344186 --- /dev/null +++ b/BotSharp.Core/Agents/AgentIntegration.cs @@ -0,0 +1,26 @@ +using EntityFrameworkCore.BootKit; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text; + +namespace BotSharp.Core.Agents +{ + [Table("Bot_AgentIntegration")] + public class AgentIntegration : DbRecord, IDbRecord + { + [Required] + [StringLength(36)] + public String AgentId { get; set; } + + [MaxLength(64)] + public String Platform { get; set; } + + [MaxLength(64)] + public String VerifyToken { get; set; } + + [MaxLength(256)] + public String AccessToken { get; set; } + } +} diff --git a/BotSharp.Core/Engines/AgentImportHeader.cs b/BotSharp.Core/Engines/AgentImportHeader.cs index b73c7d4c..42d625da 100644 --- a/BotSharp.Core/Engines/AgentImportHeader.cs +++ b/BotSharp.Core/Engines/AgentImportHeader.cs @@ -1,4 +1,5 @@ -using System; +using BotSharp.Core.Agents; +using System; using System.Collections.Generic; using System.Text; @@ -11,5 +12,10 @@ namespace BotSharp.Core.Engines public String UserId { get; set; } public String ClientAccessToken { get; set; } public String DeveloperAccessToken { get; set; } + + /// + /// Integration with other social media platform + /// + public List Integrations { get; set; } } } diff --git a/BotSharp.Core/Engines/Dialogflow/AgentImporterInDialogflow.cs b/BotSharp.Core/Engines/Dialogflow/AgentImporterInDialogflow.cs index 41581cb0..3a6a1016 100644 --- a/BotSharp.Core/Engines/Dialogflow/AgentImporterInDialogflow.cs +++ b/BotSharp.Core/Engines/Dialogflow/AgentImporterInDialogflow.cs @@ -44,6 +44,11 @@ namespace BotSharp.Core.Engines result.MlConfig = agent.ToObject(); result.MlConfig.MinConfidence = agent.MlMinConfidence; result.MlConfig.AgentId = agent.Id; + if(agentHeader.Integrations != null) + { + agentHeader.Integrations.ForEach(x => x.AgentId = agent.Id); + result.Integrations = agentHeader.Integrations; + } return result; } diff --git a/BotSharp.MachineLearning/BotSharp.MachineLearning.csproj b/BotSharp.MachineLearning/BotSharp.MachineLearning.csproj index 86ea3bbe..78982f41 100644 --- a/BotSharp.MachineLearning/BotSharp.MachineLearning.csproj +++ b/BotSharp.MachineLearning/BotSharp.MachineLearning.csproj @@ -4,4 +4,8 @@ netcoreapp2.1 + + + + diff --git a/BotSharp.Core/Engines/SpaCy/server.py b/BotSharp.MachineLearning/SpaCy/server.py similarity index 100% rename from BotSharp.Core/Engines/SpaCy/server.py rename to BotSharp.MachineLearning/SpaCy/server.py diff --git a/BotSharp.RestApi/Integrations/AmazonAlexaController.cs b/BotSharp.RestApi/Integrations/AmazonAlexaController.cs new file mode 100644 index 00000000..6aa28b45 --- /dev/null +++ b/BotSharp.RestApi/Integrations/AmazonAlexaController.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.RestApi.Integrations +{ + [Route("v1/[controller]/[action]")] + public class AmazonAlexaController : ControllerBase + { + } +} diff --git a/BotSharp.RestApi/Integrations/FacebookMessenger/FacebookMessengerController.cs b/BotSharp.RestApi/Integrations/FacebookMessenger/FacebookMessengerController.cs new file mode 100644 index 00000000..89618d0f --- /dev/null +++ b/BotSharp.RestApi/Integrations/FacebookMessenger/FacebookMessengerController.cs @@ -0,0 +1,117 @@ +using BotSharp.Core.Agents; +using BotSharp.Core.Engines; +using BotSharp.Core.Models; +using BotSharp.RestApi.Integrations.FacebookMessenger; +using DotNetToolkit; +using EntityFrameworkCore.BootKit; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using RestSharp; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace BotSharp.RestApi.Integrations +{ + [Route("v1/[controller]")] + public class FacebookMessengerController : ControllerBase + { + [HttpGet("{agentId}")] + public ActionResult Verify([FromRoute] string agentId) + { + var mode = Request.Query.ContainsKey("hub.mode") ? Request.Query["hub.mode"].ToString() : String.Empty; + var token = Request.Query.ContainsKey("hub.verify_token") ? Request.Query["hub.verify_token"].ToString() : String.Empty; + var challenge = Request.Query.ContainsKey("hub.challenge") ? Request.Query["hub.challenge"].ToString() : String.Empty; + + if (mode == "subscribe") + { + var dc = new DefaultDataContextLoader().GetDefaultDc(); + var config = dc.Table().FirstOrDefault(x => x.AgentId == agentId && x.Platform == "Facebook Messenger"); + + return config.VerifyToken == token ? Ok(challenge) : Ok(agentId); + } + + return BadRequest(); + } + + [HttpPost("{agentId}")] + public async Task CallbackAsync([FromRoute] string agentId) + { + WebhookEvent body; + IWebhookMessageBody response = null; + + using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8)) + { + var json = await reader.ReadToEndAsync(); + body = JsonConvert.DeserializeObject(json); + } + + body.Entry.ForEach(entry => + { + entry.Messaging.ForEach(msg => + { + // received text message + if (msg.Message.ContainsKey("text")) + { + OnTextMessaged(agentId, new WebhookMessage + { + Sender = msg.Sender, + Recipient = msg.Recipient, + Timestamp = msg.Timestamp, + Message = msg.Message.ToObject() + }); + } + }); + + }); + + return Ok(); + } + + private void OnTextMessaged(string agentId, WebhookMessage message) + { + Console.WriteLine($"OnTextMessaged: {message.Message.Text}"); + + var rasa = new RasaAi(); + var agent = rasa.LoadAgent(agentId); + rasa.AiConfig = new AIConfiguration(agent.ClientAccessToken, SupportedLanguage.English) { AgentId = agentId }; + rasa.AiConfig.SessionId = message.Sender.Id; + var aiResponse = rasa.TextRequest(new AIRequest { Query = new String[] { message.Message.Text } }); + + var dc = new DefaultDataContextLoader().GetDefaultDc(); + var config = dc.Table().FirstOrDefault(x => x.AgentId == agentId && x.Platform == "Facebook Messenger"); + + SendTextMessage(config.AccessToken, new WebhookMessage + { + Recipient = message.Sender.ToObject(), + Message = new WebhookTextMessage + { + Text = aiResponse.Result.Fulfillment.Speech + } + }); + } + + private void SendTextMessage(string accessToken, WebhookMessage body) + { + var client = new RestClient("https://graph.facebook.com"); + + var rest = new RestRequest("v2.6/me/messages", Method.POST); + string json = JsonConvert.SerializeObject(body, + new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + NullValueHandling = NullValueHandling.Ignore + }); + + rest.AddParameter("application/json", json, ParameterType.RequestBody); + rest.AddQueryParameter("access_token", accessToken); + + var response = client.Execute(rest); + } + } +} diff --git a/BotSharp.RestApi/Integrations/FacebookMessenger/IWebhookMessageBody.cs b/BotSharp.RestApi/Integrations/FacebookMessenger/IWebhookMessageBody.cs new file mode 100644 index 00000000..20e97b01 --- /dev/null +++ b/BotSharp.RestApi/Integrations/FacebookMessenger/IWebhookMessageBody.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.RestApi.Integrations.FacebookMessenger +{ + public interface IWebhookMessageBody + { + } +} diff --git a/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookEvent.cs b/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookEvent.cs new file mode 100644 index 00000000..86dd445a --- /dev/null +++ b/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookEvent.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.RestApi.Integrations.FacebookMessenger +{ + public class WebhookEvent + { + public string Object { get; set; } + public List Entry { get; set; } + } + + +} diff --git a/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookEventEntry.cs b/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookEventEntry.cs new file mode 100644 index 00000000..ab1cadd1 --- /dev/null +++ b/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookEventEntry.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.RestApi.Integrations.FacebookMessenger +{ + public class WebhookEventEntry + { + public string Id { get; set; } + public long Time { get; set; } + public List Messaging { get; set; } + } +} diff --git a/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookMessage.cs b/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookMessage.cs new file mode 100644 index 00000000..7dfd387b --- /dev/null +++ b/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookMessage.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.RestApi.Integrations.FacebookMessenger +{ + public class WebhookMessage + { + public WebhookMessageSender Sender { get; set; } + + public WebhookMessageRecipient Recipient { get; set; } + + public long Timestamp { get; set; } + + public JObject Message { get; set; } + } + + public class WebhookMessage where TWebhookMessage : IWebhookMessageBody + { + public WebhookMessageSender Sender { get; set; } + + public WebhookMessageRecipient Recipient { get; set; } + + public long Timestamp { get; set; } + + public TWebhookMessage Message { get; set; } + } +} diff --git a/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookMessageQuickReply.cs b/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookMessageQuickReply.cs new file mode 100644 index 00000000..9abccb34 --- /dev/null +++ b/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookMessageQuickReply.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.RestApi.Integrations.FacebookMessenger +{ + public class WebhookMessageQuickReply + { + public String Payload { get; set; } + } +} diff --git a/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookMessageRecipient.cs b/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookMessageRecipient.cs new file mode 100644 index 00000000..ecea618e --- /dev/null +++ b/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookMessageRecipient.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.RestApi.Integrations.FacebookMessenger +{ + public class WebhookMessageRecipient + { + /// + /// PAGE_ID + /// + public String Id { get; set; } + } +} diff --git a/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookMessageSender.cs b/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookMessageSender.cs new file mode 100644 index 00000000..ea1b37ad --- /dev/null +++ b/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookMessageSender.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.RestApi.Integrations.FacebookMessenger +{ + public class WebhookMessageSender + { + /// + /// PSID + /// + public String Id { get; set; } + } +} diff --git a/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookTextMessage.cs b/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookTextMessage.cs new file mode 100644 index 00000000..b7e8b1d3 --- /dev/null +++ b/BotSharp.RestApi/Integrations/FacebookMessenger/WebhookTextMessage.cs @@ -0,0 +1,15 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.RestApi.Integrations.FacebookMessenger +{ + public class WebhookTextMessage : IWebhookMessageBody + { + public string Mid { get; set; } + public String Text { get; set; } + [JsonProperty("quick_reply")] + public WebhookMessageQuickReply QuickReply { get; set; } + } +} diff --git a/BotSharp.RestApi/Integrations/SkypeController.cs b/BotSharp.RestApi/Integrations/SkypeController.cs new file mode 100644 index 00000000..13480b65 --- /dev/null +++ b/BotSharp.RestApi/Integrations/SkypeController.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.RestApi.Integrations +{ + [Route("v1/[controller]/[action]")] + public class SkypeController : ControllerBase + { + } +} diff --git a/BotSharp.RestApi/Integrations/SlackController.cs b/BotSharp.RestApi/Integrations/SlackController.cs new file mode 100644 index 00000000..ebfca15d --- /dev/null +++ b/BotSharp.RestApi/Integrations/SlackController.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.RestApi.Integrations +{ + [Route("v1/[controller]/[action]")] + public class SlackController : ControllerBase + { + } +} diff --git a/BotSharp.RestApi/FacebookMessengerController.cs b/BotSharp.RestApi/Integrations/TelegramController.cs similarity index 50% rename from BotSharp.RestApi/FacebookMessengerController.cs rename to BotSharp.RestApi/Integrations/TelegramController.cs index c01d3d4c..76539255 100644 --- a/BotSharp.RestApi/FacebookMessengerController.cs +++ b/BotSharp.RestApi/Integrations/TelegramController.cs @@ -3,15 +3,11 @@ using System; using System.Collections.Generic; using System.Text; -namespace BotSharp.RestApi +namespace BotSharp.RestApi.Integrations { [Route("v1/[controller]/[action]")] - public class FacebookMessengerController : ControllerBase + public class TelegramController : ControllerBase { - [HttpGet] - public void Facebook() - { - } } } diff --git a/BotSharp.RestApi/Integrations/TwitterController.cs b/BotSharp.RestApi/Integrations/TwitterController.cs new file mode 100644 index 00000000..9e72dce7 --- /dev/null +++ b/BotSharp.RestApi/Integrations/TwitterController.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.RestApi.Integrations +{ + [Route("v1/[controller]/[action]")] + public class TwitterController : ControllerBase + { + } +} diff --git a/BotSharp.RestApi/Integrations/ViberController.cs b/BotSharp.RestApi/Integrations/ViberController.cs new file mode 100644 index 00000000..6d424f82 --- /dev/null +++ b/BotSharp.RestApi/Integrations/ViberController.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; +using System; +using System.Collections.Generic; +using System.Text; + +namespace BotSharp.RestApi.Integrations +{ + [Route("v1/[controller]/[action]")] + public class ViberController : ControllerBase + { + } +} diff --git a/BotSharp.UnitTest/ConversationTest.cs b/BotSharp.UnitTest/ConversationTest.cs index c412c005..2d70c4fb 100644 --- a/BotSharp.UnitTest/ConversationTest.cs +++ b/BotSharp.UnitTest/ConversationTest.cs @@ -16,9 +16,8 @@ namespace BotSharp.UnitTest { var rasa = new RasaAi(); var agent = rasa.LoadAgent(BOT_ID); - - var config = new AIConfiguration(agent.ClientAccessToken, SupportedLanguage.English) { AgentId = BOT_ID }; - config.SessionId = Guid.NewGuid().ToString(); + rasa.AiConfig = new AIConfiguration(agent.ClientAccessToken, SupportedLanguage.English) { AgentId = BOT_ID }; + rasa.AiConfig.SessionId = Guid.NewGuid().ToString(); // Round 1 var response = rasa.TextRequest(new AIRequest { Query = new String[] { "Can you play country music?" } }); diff --git a/BotSharp.WebHost/App_Data/DbInitializer/Agents/agents.json b/BotSharp.WebHost/App_Data/DbInitializer/Agents/agents.json index 4793960a..2f455665 100644 --- a/BotSharp.WebHost/App_Data/DbInitializer/Agents/agents.json +++ b/BotSharp.WebHost/App_Data/DbInitializer/Agents/agents.json @@ -8,8 +8,9 @@ "Integrations": [ { - "Platform": "FacebookMessenger", - "PageAccessToken": "EAAJym8gGQFMBAPnsxTw6rZBE2WVfYtCJeGkCQ2NZC3VbQd45SyUlXjjgUf1gAkCOWq7v0blxTb1xLZAeMAXYhQj3btcMlMO9YbG7StMyx8ussz5TnD1tjjIZCye5u66PZAuFxSyIPXtTCin8GPiJ19cYB8ZCyH3rr5sro7OxUSyAZDZD" + "Platform": "Facebook Messenger", + "VerifyToken": "spotify_music_bot", + "AccessToken": "EAAJym8gGQFMBAPnsxTw6rZBE2WVfYtCJeGkCQ2NZC3VbQd45SyUlXjjgUf1gAkCOWq7v0blxTb1xLZAeMAXYhQj3btcMlMO9YbG7StMyx8ussz5TnD1tjjIZCye5u66PZAuFxSyIPXtTCin8GPiJ19cYB8ZCyH3rr5sro7OxUSyAZDZD" } ] } diff --git a/BotSharp.WebHost/wwwroot/images/spotify-clip.png b/BotSharp.WebHost/wwwroot/images/spotify-clip.png new file mode 100644 index 0000000000000000000000000000000000000000..6f52b4023b7dcd81c46c71030376b08605e5060f GIT binary patch literal 7089 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