diff --git a/Microsoft.Bot.Builder.sln b/Microsoft.Bot.Builder.sln index 556a403bd..236f64bc7 100644 --- a/Microsoft.Bot.Builder.sln +++ b/Microsoft.Bot.Builder.sln @@ -165,6 +165,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Adapt EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Adapters.Facebook", "libraries\Adapters\Microsoft.Bot.Builder.Adapters.Facebook\Microsoft.Bot.Builder.Adapters.Facebook.csproj", "{C8E31CD2-89D4-4659-9557-43EC9C99D984}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot", "tests\Adapters\Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot\Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot.csproj", "{1D05EFE4-7F25-4D5A-BCEE-1109B9EF25A8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug - NuGet Packages|Any CPU = Debug - NuGet Packages|Any CPU @@ -796,6 +798,16 @@ Global {C8E31CD2-89D4-4659-9557-43EC9C99D984}.Release|Any CPU.Build.0 = Release|Any CPU {C8E31CD2-89D4-4659-9557-43EC9C99D984}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU {C8E31CD2-89D4-4659-9557-43EC9C99D984}.Release-Windows|Any CPU.Build.0 = Release|Any CPU + {1D05EFE4-7F25-4D5A-BCEE-1109B9EF25A8}.Debug - NuGet Packages|Any CPU.ActiveCfg = Debug|Any CPU + {1D05EFE4-7F25-4D5A-BCEE-1109B9EF25A8}.Debug - NuGet Packages|Any CPU.Build.0 = Debug|Any CPU + {1D05EFE4-7F25-4D5A-BCEE-1109B9EF25A8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1D05EFE4-7F25-4D5A-BCEE-1109B9EF25A8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1D05EFE4-7F25-4D5A-BCEE-1109B9EF25A8}.Debug-Windows|Any CPU.ActiveCfg = Debug|Any CPU + {1D05EFE4-7F25-4D5A-BCEE-1109B9EF25A8}.Debug-Windows|Any CPU.Build.0 = Debug|Any CPU + {1D05EFE4-7F25-4D5A-BCEE-1109B9EF25A8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1D05EFE4-7F25-4D5A-BCEE-1109B9EF25A8}.Release|Any CPU.Build.0 = Release|Any CPU + {1D05EFE4-7F25-4D5A-BCEE-1109B9EF25A8}.Release-Windows|Any CPU.ActiveCfg = Release|Any CPU + {1D05EFE4-7F25-4D5A-BCEE-1109B9EF25A8}.Release-Windows|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -869,6 +881,7 @@ Global {9F7B2BDF-973B-4639-B890-357EB967B2ED} = {E8CD434A-306F-41D9-B67D-BFFF3287354D} {E372D54C-73AA-41DB-A471-81348AC37F36} = {E8CD434A-306F-41D9-B67D-BFFF3287354D} {C8E31CD2-89D4-4659-9557-43EC9C99D984} = {6230B915-B238-4E57-AAC4-06B4498F540F} + {1D05EFE4-7F25-4D5A-BCEE-1109B9EF25A8} = {E8CD434A-306F-41D9-B67D-BFFF3287354D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7173C9F3-A7F9-496E-9078-9156E35D6E16} diff --git a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookAdapter.cs b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookAdapter.cs index d4e113cb8..e88e76ce9 100644 --- a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookAdapter.cs +++ b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookAdapter.cs @@ -20,6 +20,11 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook public class FacebookAdapter : BotAdapter, IBotFrameworkHttpAdapter { private const string HubModeSubscribe = "subscribe"; + + /// + /// The constant ID representing the page inbox. + /// + private const string PageInboxId = "263902037430900"; private readonly FacebookClientWrapper _facebookClient; @@ -61,9 +66,9 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook foreach (var activity in activities) { - if (activity.Type != ActivityTypes.Message) + if (activity.Type != ActivityTypes.Message && activity.Type != ActivityTypes.Event) { - throw new Exception("Only Activities of type Message are supported for sending."); + continue; } var message = FacebookHelper.ActivityToFacebook(activity); @@ -76,6 +81,23 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook var res = await _facebookClient.SendMessageAsync("/me/messages", message, null, cancellationToken).ConfigureAwait(false); + if (activity.Type == ActivityTypes.Event) + { + if (activity.Name.Equals("pass_thread_control", StringComparison.InvariantCulture)) + { + var recipient = (string)activity.Value == "inbox" ? PageInboxId : (string)activity.Value; + await _facebookClient.PassThreadControlAsync(recipient, activity.Conversation.Id, "Pass thread control").ConfigureAwait(false); + } + else if (activity.Name.Equals("take_thread_control", StringComparison.InvariantCulture)) + { + await _facebookClient.TakeThreadControlAsync(activity.Conversation.Id, "Take thread control from a secondary receiver").ConfigureAwait(false); + } + else if (activity.Name.Equals("request_thread_control", StringComparison.InvariantCulture)) + { + await _facebookClient.RequestThreadControlAsync(activity.Conversation.Id, "Request thread control to the primary receiver").ConfigureAwait(false); + } + } + var response = new ResourceResponse() { Id = res, @@ -153,31 +175,33 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook return; } - string stringifyBody; + string stringifiedBody; using (var sr = new StreamReader(request.Body)) { - stringifyBody = sr.ReadToEnd(); + stringifiedBody = sr.ReadToEnd(); } - if (!_facebookClient.VerifySignature(request, stringifyBody)) + if (!_facebookClient.VerifySignature(request, stringifiedBody)) { await FacebookHelper.WriteAsync(response, HttpStatusCode.Unauthorized, string.Empty, Encoding.UTF8, cancellationToken).ConfigureAwait(false); throw new Exception("WARNING: Webhook received message with invalid signature. Potential malicious behavior!"); } - FacebookResponseEvent facebookEvent = null; + FacebookResponseEvent facebookResponseEvent = null; - facebookEvent = JsonConvert.DeserializeObject(stringifyBody); + facebookResponseEvent = JsonConvert.DeserializeObject(stringifiedBody); - foreach (var entry in facebookEvent.Entry) + foreach (var entry in facebookResponseEvent.Entry) { var payload = new List(); - payload = entry.Changes ?? entry.Messaging; + payload = entry.Changes ?? entry.Messaging ?? entry.Standby; foreach (var message in payload) { + message.IsStandby = entry.Standby != null; + var activity = FacebookHelper.ProcessSingleMessage(message); using (var context = new TurnContext(this, activity)) @@ -185,24 +209,6 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook await RunPipelineAsync(context, bot.OnTurnAsync, cancellationToken).ConfigureAwait(false); } } - - // Handle standby messages (this bot is not the active receiver) - if (entry.Standby != null) - { - payload = entry.Standby; - - foreach (var message in payload) - { - // Indicate that this message was received in standby mode rather than normal mode. - message.Standby = true; - var activity = FacebookHelper.ProcessSingleMessage(message); - - using (var context = new TurnContext(this, activity)) - { - await RunPipelineAsync(context, bot.OnTurnAsync, cancellationToken).ConfigureAwait(false); - } - } - } } } } diff --git a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookClientWrapper.cs b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookClientWrapper.cs index 2ddcc88a8..6b38b3487 100644 --- a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookClientWrapper.cs +++ b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookClientWrapper.cs @@ -159,5 +159,99 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook await FacebookHelper.WriteAsync(response, statusCode, challenge, Encoding.UTF8, cancellationToken).ConfigureAwait(false); } + + /// + /// Posts webhook control events to Facebook. + /// + /// The REST post type (GET, PUT, POST, etc). + /// The string content to be posted to Facebook. + /// A bool indicating the success of the operation. + public virtual async Task PostToFacebookApiAsync(string postType, string content) + { + if (postType == null) + { + throw new ArgumentNullException(nameof(postType)); + } + + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + var graphApiBaseUrl = $"https://{_options.FacebookApiHost}/{_options.FacebookApiVersion + postType}?access_token={_options.FacebookAccessToken}"; + var requestPath = string.Format(System.Globalization.CultureInfo.InvariantCulture, graphApiBaseUrl, postType, _options.FacebookAccessToken); + var stringContent = new StringContent(content, Encoding.UTF8, "application/json"); + + using (var requestMessage = new HttpRequestMessage()) + { + requestMessage.Method = new HttpMethod("POST"); + requestMessage.RequestUri = new Uri(requestPath); + requestMessage.Content = stringContent; + requestMessage.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json; charset=utf-8"); + + using (var client = new HttpClient()) + { + var res = await client.SendAsync(requestMessage, CancellationToken.None).ConfigureAwait(false); + return res.IsSuccessStatusCode; + } + } + } + + /// + /// Sends the request_thread_control webhook event to Facebook. + /// + /// The sender user Id. + /// An optional message for the metadata paremter. + /// A bool value indicating the success of the operation. + public virtual async Task RequestThreadControlAsync(string userId, string message) + { + if (userId == null) + { + throw new ArgumentNullException(nameof(userId)); + } + + var content = new { recipient = new { id = userId }, metadata = message }; + return await PostToFacebookApiAsync("/me/request_thread_control", JsonConvert.SerializeObject(content)).ConfigureAwait(false); + } + + /// + /// Sends the take_thread_control webhook event to Facebook. + /// + /// The sender user Id. + /// An optional message for the metadata paremter. + /// A bool value indicating the success of the operation. + public virtual async Task TakeThreadControlAsync(string userId, string message) + { + if (userId == null) + { + throw new ArgumentNullException(nameof(userId)); + } + + var content = new { recipient = new { id = userId }, metadata = message }; + return await PostToFacebookApiAsync("/me/take_thread_control", JsonConvert.SerializeObject(content)).ConfigureAwait(false); + } + + /// + /// Sends the pass_thread_control webhook event to Facebook. + /// + /// The Id of the target app to pass control to. + /// The sender user Id. + /// An optional message for the metadata paremter. + /// A bool value indicating the success of the operation. + public virtual async Task PassThreadControlAsync(string targetAppId, string userId, string message) + { + if (targetAppId == null) + { + throw new ArgumentNullException(nameof(targetAppId)); + } + + if (userId == null) + { + throw new ArgumentNullException(nameof(userId)); + } + + var content = new { recipient = new { id = userId }, target_app_id = targetAppId, metadata = message }; + return await PostToFacebookApiAsync("/me/pass_thread_control", JsonConvert.SerializeObject(content)).ConfigureAwait(false); + } } } diff --git a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/MessagePayload.cs b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/AttachmentPayload.cs similarity index 98% rename from libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/MessagePayload.cs rename to libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/AttachmentPayload.cs index c99eda138..bcf5f7929 100644 --- a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/MessagePayload.cs +++ b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/AttachmentPayload.cs @@ -8,7 +8,7 @@ using Newtonsoft.Json; namespace Microsoft.Bot.Builder.Adapters.Facebook.FacebookEvents { - public class MessagePayload + public class AttachmentPayload { /// /// Gets or sets the url of the attachment. diff --git a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/FacebookAttachment.cs b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/FacebookAttachment.cs index 162b01860..74ef5b125 100644 --- a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/FacebookAttachment.cs +++ b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/FacebookAttachment.cs @@ -19,6 +19,6 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook.FacebookEvents /// /// Payload of the attachment. [JsonProperty(PropertyName = "payload")] - public MessagePayload Payload { get; set; } + public AttachmentPayload Payload { get; set; } } } diff --git a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/FacebookEntry.cs b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/FacebookEntry.cs index 58d7fc88a..ee14b41fe 100644 --- a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/FacebookEntry.cs +++ b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/FacebookEntry.cs @@ -20,21 +20,21 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook.FacebookEvents public long Time { get; set; } /// - /// Gets or sets the messaging list. + /// Gets the messaging list. /// /// List containing one messaging object. Note that even though this is an enumerable, it will only contain one object. - public List Messaging { get; set; } + public List Messaging { get; } /// - /// Gets or sets the changes list. + /// Gets the changes list. /// /// List containing the list of changes. - public List Changes { get; set; } + public List Changes { get; } /// - /// Gets or sets the standby messages list. + /// Gets the standby messages list. /// /// List containing the messages sent while in standby mode. - public List Standby { get; set; } + public List Standby { get; } } } diff --git a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/FacebookResponseEvent.cs b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/FacebookResponseEvent.cs index 2ded350a5..e573d75d7 100644 --- a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/FacebookResponseEvent.cs +++ b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/FacebookResponseEvent.cs @@ -11,6 +11,6 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook.FacebookEvents [JsonProperty(PropertyName = "object")] public string ResponseObject { get; set; } - public List Entry { get; set; } + public List Entry { get; } } } diff --git a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/Handover/FacebookPassThreadControl.cs b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/Handover/FacebookPassThreadControl.cs new file mode 100644 index 000000000..25c4ea727 --- /dev/null +++ b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/Handover/FacebookPassThreadControl.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Builder.Adapters.Facebook.FacebookEvents.Handover +{ + public class FacebookPassThreadControl : FacebookThreadControl + { + /// + /// Gets or Sets the app id of the new owner. + /// + /// + /// 263902037430900 for the page inbox. + /// + /// + /// The app id of the new owner. + /// + [JsonProperty("new_owner_app_id")] + public string NewOwnerAppId { get; set; } + + [JsonProperty("previous_owner_app_id")] + public string PreviousOwnerAppId { get; set; } + + [JsonProperty("requested_owner_app_id")] + public string RequestedOwnerAppId { get; set; } + } +} diff --git a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/Handover/FacebookRequestThreadControl.cs b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/Handover/FacebookRequestThreadControl.cs new file mode 100644 index 000000000..f4c0823a1 --- /dev/null +++ b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/Handover/FacebookRequestThreadControl.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Builder.Adapters.Facebook.FacebookEvents.Handover +{ + /// + /// A Facebook thread control message, including appid of requested thread owner and an optional message to send with the request + /// . + /// + public class FacebookRequestThreadControl : FacebookThreadControl + { + /// + /// Gets or Sets the app id of the requested owner. + /// + /// + /// 263902037430900 for the page inbox. + /// + /// + /// the app id of the requested owner. + /// + [JsonProperty("requested_owner_app_id")] + public string RequestedOwnerAppId { get; set; } + } +} diff --git a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/Handover/FacebookTakeThreadControl.cs b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/Handover/FacebookTakeThreadControl.cs new file mode 100644 index 000000000..3f6339e85 --- /dev/null +++ b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/Handover/FacebookTakeThreadControl.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Builder.Adapters.Facebook.FacebookEvents.Handover +{ + public class FacebookTakeThreadControl : FacebookThreadControl + { + /// + /// Gets or Sets the app id of the previous owner. + /// + /// + /// 263902037430900 for the page inbox. + /// + /// + /// The app id of the previous owner. + /// + [JsonProperty("previous_owner_app_id")] + public string PreviousOwnerAppId { get; set; } + } +} diff --git a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/Handover/FacebookThreadControl.cs b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/Handover/FacebookThreadControl.cs new file mode 100644 index 000000000..2c85e4da6 --- /dev/null +++ b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookEvents/Handover/FacebookThreadControl.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Builder.Adapters.Facebook.FacebookEvents.Handover +{ + public class FacebookThreadControl + { + /// + /// Gets or Sets the message sent from the requester. + /// + /// + /// Example: "All yours!". + /// + /// + /// Message sent from the requester. + /// + [JsonProperty("metadata")] + public string Metadata { get; set; } + } +} diff --git a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookHelper.cs b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookHelper.cs index c3ed7d1e3..b87912dec 100644 --- a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookHelper.cs +++ b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookHelper.cs @@ -32,7 +32,6 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook facebookMessage.Message.Text = activity.Text; - // map these fields to their appropriate place if (activity.ChannelData != null) { facebookMessage = activity.GetChannelData(); @@ -52,7 +51,7 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook if (activity.Attachments != null && activity.Attachments.Count > 0) { - var payload = JsonConvert.DeserializeObject(JsonConvert.SerializeObject( + var payload = JsonConvert.DeserializeObject(JsonConvert.SerializeObject( activity.Attachments[0].Content, Formatting.None, new JsonSerializerSettings @@ -60,13 +59,13 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook NullValueHandling = NullValueHandling.Ignore, })); - var attach = new FacebookAttachment + var facebookAttachment = new FacebookAttachment { Type = activity.Attachments[0].ContentType, Payload = payload, }; - facebookMessage.Message.Attachment = attach; + facebookMessage.Message.Attachment = facebookAttachment; } return facebookMessage; @@ -112,18 +111,24 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook Text = null, }; + if (message.PassThreadControl != null) + { + activity.Value = message.PassThreadControl; + } + else if (message.RequestThreadControl != null) + { + activity.Value = message.RequestThreadControl; + } + else if (message.TakeThreadControl != null) + { + activity.Value = message.TakeThreadControl; + } + if (message.Message != null) { - activity.Type = ActivityTypes.Message; activity.Text = message.Message.Text; - - if (activity.GetChannelData().Message.IsEcho) - { - activity.Type = ActivityTypes.Event; - } - - // copy all fields (like attachments, sticker, quick_reply, nlp, etc.) - activity.ChannelData = message.Message; + activity.Type = activity.GetChannelData().Message.IsEcho ? ActivityTypes.Event : ActivityTypes.Message; + if (message.Message.Attachments != null && message.Message.Attachments.Count > 0) { activity.Attachments = HandleMessageAttachments(message.Message); @@ -138,6 +143,11 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook return activity; } + /// + /// Extracts attachments from the facebook message. + /// + /// The used for input. + /// A List of . public static List HandleMessageAttachments(Message message) { var attachmentsList = new List(); diff --git a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookMessage.cs b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookMessage.cs index 02491a6e0..46b3d412c 100644 --- a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookMessage.cs +++ b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/FacebookMessage.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Microsoft.Bot.Builder.Adapters.Facebook.FacebookEvents; +using Microsoft.Bot.Builder.Adapters.Facebook.FacebookEvents.Handover; using Newtonsoft.Json; namespace Microsoft.Bot.Builder.Adapters.Facebook @@ -79,16 +80,53 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook [JsonProperty(PropertyName = "sender_action")] public string SenderAction { get; set; } + /// + /// Gets or sets the timestamp. + /// + /// Timestamp. [JsonProperty(PropertyName = "timestamp")] public long TimeStamp { get; set; } + /// + /// Gets or sets a value indicating whether the message was received while in Standby mode. + /// + /// Value indicating whether the message was received while in Standby mode. [JsonProperty(PropertyName = "standby")] - public bool Standby { get; set; } + public bool IsStandby { get; set; } + /// + /// Gets or sets the value of the postback property. + /// + /// The postback payload. Postbacks occur when a postback button, Get Started button, or persistent menu item is tapped. [JsonProperty(PropertyName = "postback")] public FacebookPostBack PostBack { get; set; } + /// + /// Gets or sets the value of the optin property. + /// + /// The optin field. See https://developers.facebook.com/docs/messenger-platform/reference/webhook-events/messaging_optins. [JsonProperty(PropertyName = "optin")] public FacebookRecipient OptIn { get; set; } + + /// + /// Gets or sets the contents of the pass_thread_control property. + /// + /// A holding the contents of the pass_thread_control property.. + [JsonProperty(PropertyName = "pass_thread_control")] + public FacebookPassThreadControl PassThreadControl { get; set; } + + /// + /// Gets or sets the contents of the take_thread_control property. + /// + /// A holding the contents of the pass_thread_control property.. + [JsonProperty(PropertyName = "take_thread_control")] + public FacebookTakeThreadControl TakeThreadControl { get; set; } + + /// + /// Gets or sets the contents of the request_thread_control property. + /// + /// A holding the contents of the pass_thread_control property.. + [JsonProperty(PropertyName = "request_thread_control")] + public FacebookRequestThreadControl RequestThreadControl { get; set; } } } diff --git a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/Message.cs b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/Message.cs index e196d51e9..e66601bc7 100644 --- a/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/Message.cs +++ b/libraries/Adapters/Microsoft.Bot.Builder.Adapters.Facebook/Message.cs @@ -26,14 +26,14 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook /// /// Gets or sets a list of attachments. /// - /// Attachments. + /// Attachments that could come with a Facebook message. [JsonProperty(PropertyName = "attachments")] public List Attachments { get; set; } /// /// Gets or sets the attachment. /// - /// Attachment. + /// Single attachment that will be sent back to Facebook. [JsonProperty(PropertyName = "attachment")] public FacebookAttachment Attachment { get; set; } @@ -52,9 +52,20 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook [JsonProperty(PropertyName = "quick_replies")] public List QuickReplies { get; set; } = new List(); + /// + /// Gets or sets a value indicating whether the message was sent from the page itself. + /// + /// A value indicating whether the message was sent from the page itself. [JsonProperty(PropertyName = "is_echo")] public bool IsEcho { get; set; } + /// + /// Gets or sets the Mid. + /// + /// Message ID. + [JsonProperty(PropertyName = "mid")] + public string Mid { get; set; } + public bool ShouldSerializeQuickReplies() { return QuickReplies.Count > 0; diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Bots/EchoBot.cs b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Bots/EchoBot.cs new file mode 100644 index 000000000..2d8650106 --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Bots/EchoBot.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// +// Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.3.0 + +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Bot.Builder.Adapters.Facebook.FacebookEvents.Handover; +using Microsoft.Bot.Schema; +using Newtonsoft.Json; + +namespace Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot.Bots +{ + public class EchoBot : ActivityHandler + { + /// + /// This option passes thread control from the secondary receiver to a primary receiver. + /// + private const string OptionPassPrimaryBot = "Pass to primary"; + + protected override async Task OnMembersAddedAsync(IList membersAdded, ITurnContext turnContext, CancellationToken cancellationToken) + { + var activity = MessageFactory.Text("Hello and Welcome!"); + await turnContext.SendActivityAsync(activity, cancellationToken); + } + + protected override async Task OnMessageActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) + { + if (turnContext.Activity.GetChannelData().IsStandby) + { + if ((turnContext.Activity as Activity)?.Text == "OtherBot") + { + var activity = new Activity(); + activity.Type = ActivityTypes.Event; + + //Action + (activity as IEventActivity).Name = "request_thread_control"; + await turnContext.SendActivityAsync(activity, cancellationToken); + } + } + else if (turnContext.Activity.Attachments != null) + { + foreach (var attachment in turnContext.Activity.Attachments) + { + var activity = MessageFactory.Text($" I got {turnContext.Activity.Attachments.Count} attachments"); + + var image = new Attachment( + attachment.ContentType, + content: attachment.Content); + + activity.Attachments.Add(image); + await turnContext.SendActivityAsync(activity, cancellationToken); + } + } + else + { + IActivity activity; + + switch (turnContext.Activity.Text) + { + case OptionPassPrimaryBot: + activity = MessageFactory.Text("Redirecting to the primary bot..."); + activity.Type = ActivityTypes.Event; + (activity as IEventActivity).Name = "pass_thread_control"; + (activity as IEventActivity).Value = ""; + break; + + case "Redirected to the bot": + activity = MessageFactory.Text("Hello Human, I'm the secondary bot to help you!"); + break; + case "Little": + activity = MessageFactory.Text($"You have spoken the forbidden word!"); + break; + default: + activity = MessageFactory.Text($"Echo Secondary: {turnContext.Activity.Text}"); + break; + } + + await turnContext.SendActivityAsync(activity, cancellationToken); + } + } + + protected override async Task OnEventActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) + { + if (turnContext.Activity.Value != null) + { + var metadata = (turnContext.Activity.Value as FacebookThreadControl).Metadata; + + if (metadata.Equals("Pass thread control")) + { + var activity = MessageFactory.Text("Hello Human, I'm the secondary bot to help you!"); + await turnContext.SendActivityAsync(activity, cancellationToken); + } + } + } + + private static Attachment CreateTemplateAttachment(string filePath) + { + var templateAttachmentJson = File.ReadAllText(filePath); + var templateAttachment = new Attachment() + { + ContentType = "template", + Content = JsonConvert.DeserializeObject(templateAttachmentJson), + }; + return templateAttachment; + } + } +} diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Controllers/BotController.cs b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Controllers/BotController.cs new file mode 100644 index 000000000..e300b012c --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Controllers/BotController.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// +// Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.3.0 + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Bot.Builder.Integration.AspNet.Core; + +namespace Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot.Controllers +{ + // This ASP Controller is created to handle a request. Dependency Injection will provide the Adapter and IBot + // implementation at runtime. Multiple different IBot implementations running at different endpoints can be + // achieved by specifying a more specific type for the bot constructor argument. + [Route("api/messages")] + [ApiController] + public class BotController : ControllerBase + { + private readonly IBotFrameworkHttpAdapter _adapter; + private readonly IBot _bot; + + public BotController(IBotFrameworkHttpAdapter adapter, IBot bot) + { + _adapter = adapter; + _bot = bot; + } + + [HttpPost] + public async Task PostAsync() + { + // Delegate the processing of the HTTP POST to the adapter. + // The adapter will invoke the bot. + await _adapter.ProcessAsync(Request, Response, _bot); + } + + [HttpGet] + public async Task GetAsync() + { + // Delegate the processing of the HTTP POST to the adapter. + // The adapter will invoke the bot. + await _adapter.ProcessAsync(Request, Response, _bot); + } + } +} diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/DeploymentTemplates/template-with-new-rg.json b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/DeploymentTemplates/template-with-new-rg.json new file mode 100644 index 000000000..06b828415 --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/DeploymentTemplates/template-with-new-rg.json @@ -0,0 +1,183 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "groupLocation": { + "type": "string", + "metadata": { + "description": "Specifies the location of the Resource Group." + } + }, + "groupName": { + "type": "string", + "metadata": { + "description": "Specifies the name of the Resource Group." + } + }, + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "metadata": { + "description": "The name of the App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "newAppServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan. Defaults to \"westus\"." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "appServicePlanName": "[parameters('newAppServicePlanName')]", + "resourcesLocation": "[parameters('newAppServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "name": "[parameters('groupName')]", + "type": "Microsoft.Resources/resourceGroups", + "apiVersion": "2018-05-01", + "location": "[parameters('groupLocation')]", + "properties": { + } + }, + { + "type": "Microsoft.Resources/deployments", + "apiVersion": "2018-05-01", + "name": "storageDeployment", + "resourceGroup": "[parameters('groupName')]", + "dependsOn": [ + "[resourceId('Microsoft.Resources/resourceGroups/', parameters('groupName'))]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "comments": "Create a new App Service Plan", + "type": "Microsoft.Web/serverfarms", + "name": "[variables('appServicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "properties": { + "name": "[variables('appServicePlanName')]" + } + }, + { + "comments": "Create a Web App using the new App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "location": "[variables('resourcesLocation')]", + "kind": "app", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('appServicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "serverFarmId": "[variables('appServicePlanName')]", + "siteConfig": { + "appSettings": [ + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "10.14.1" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ], + "outputs": {} + } + } + } + ] +} \ No newline at end of file diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/DeploymentTemplates/template-with-preexisting-rg.json b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/DeploymentTemplates/template-with-preexisting-rg.json new file mode 100644 index 000000000..43943b658 --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/DeploymentTemplates/template-with-preexisting-rg.json @@ -0,0 +1,154 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "appId": { + "type": "string", + "metadata": { + "description": "Active Directory App ID, set as MicrosoftAppId in the Web App's Application Settings." + } + }, + "appSecret": { + "type": "string", + "metadata": { + "description": "Active Directory App Password, set as MicrosoftAppPassword in the Web App's Application Settings. Defaults to \"\"." + } + }, + "botId": { + "type": "string", + "metadata": { + "description": "The globally unique and immutable bot ID. Also used to configure the displayName of the bot, which is mutable." + } + }, + "botSku": { + "defaultValue": "F0", + "type": "string", + "metadata": { + "description": "The pricing tier of the Bot Service Registration. Acceptable values are F0 and S1." + } + }, + "newAppServicePlanName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The name of the new App Service Plan." + } + }, + "newAppServicePlanSku": { + "type": "object", + "defaultValue": { + "name": "S1", + "tier": "Standard", + "size": "S1", + "family": "S", + "capacity": 1 + }, + "metadata": { + "description": "The SKU of the App Service Plan. Defaults to Standard values." + } + }, + "appServicePlanLocation": { + "type": "string", + "metadata": { + "description": "The location of the App Service Plan." + } + }, + "existingAppServicePlan": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "Name of the existing App Service Plan used to create the Web App for the bot." + } + }, + "newWebAppName": { + "type": "string", + "defaultValue": "", + "metadata": { + "description": "The globally unique name of the Web App. Defaults to the value passed in for \"botId\"." + } + } + }, + "variables": { + "defaultAppServicePlanName": "[if(empty(parameters('existingAppServicePlan')), 'createNewAppServicePlan', parameters('existingAppServicePlan'))]", + "useExistingAppServicePlan": "[not(equals(variables('defaultAppServicePlanName'), 'createNewAppServicePlan'))]", + "servicePlanName": "[if(variables('useExistingAppServicePlan'), parameters('existingAppServicePlan'), parameters('newAppServicePlanName'))]", + "resourcesLocation": "[parameters('appServicePlanLocation')]", + "webAppName": "[if(empty(parameters('newWebAppName')), parameters('botId'), parameters('newWebAppName'))]", + "siteHost": "[concat(variables('webAppName'), '.azurewebsites.net')]", + "botEndpoint": "[concat('https://', variables('siteHost'), '/api/messages')]" + }, + "resources": [ + { + "comments": "Create a new App Service Plan if no existing App Service Plan name was passed in.", + "type": "Microsoft.Web/serverfarms", + "condition": "[not(variables('useExistingAppServicePlan'))]", + "name": "[variables('servicePlanName')]", + "apiVersion": "2018-02-01", + "location": "[variables('resourcesLocation')]", + "sku": "[parameters('newAppServicePlanSku')]", + "properties": { + "name": "[variables('servicePlanName')]" + } + }, + { + "comments": "Create a Web App using an App Service Plan", + "type": "Microsoft.Web/sites", + "apiVersion": "2015-08-01", + "location": "[variables('resourcesLocation')]", + "kind": "app", + "dependsOn": [ + "[resourceId('Microsoft.Web/serverfarms/', variables('servicePlanName'))]" + ], + "name": "[variables('webAppName')]", + "properties": { + "name": "[variables('webAppName')]", + "serverFarmId": "[variables('servicePlanName')]", + "siteConfig": { + "appSettings": [ + { + "name": "WEBSITE_NODE_DEFAULT_VERSION", + "value": "10.14.1" + }, + { + "name": "MicrosoftAppId", + "value": "[parameters('appId')]" + }, + { + "name": "MicrosoftAppPassword", + "value": "[parameters('appSecret')]" + } + ], + "cors": { + "allowedOrigins": [ + "https://botservice.hosting.portal.azure.net", + "https://hosting.onecloud.azure-test.net/" + ] + } + } + } + }, + { + "apiVersion": "2017-12-01", + "type": "Microsoft.BotService/botServices", + "name": "[parameters('botId')]", + "location": "global", + "kind": "bot", + "sku": { + "name": "[parameters('botSku')]" + }, + "properties": { + "name": "[parameters('botId')]", + "displayName": "[parameters('botId')]", + "endpoint": "[variables('botEndpoint')]", + "msaAppId": "[parameters('appId')]", + "developerAppInsightsApplicationId": null, + "developerAppInsightKey": null, + "publishingCredentials": null, + "storageResourceId": null + }, + "dependsOn": [ + "[resourceId('Microsoft.Web/sites/', variables('webAppName'))]" + ] + } + ] +} \ No newline at end of file diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot.csproj b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot.csproj new file mode 100644 index 000000000..ea925b60c --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot.csproj @@ -0,0 +1,26 @@ + + + + netcoreapp2.1 + + + + + + + + + + + + + + + Always + + + PreserveNewest + + + + diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot.sln b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot.sln new file mode 100644 index 000000000..03ca9b589 --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29102.190 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot", "Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot.csproj", "{29FEF930-5333-41D4-8035-D052F056BD65}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {29FEF930-5333-41D4-8035-D052F056BD65}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {29FEF930-5333-41D4-8035-D052F056BD65}.Debug|Any CPU.Build.0 = Debug|Any CPU + {29FEF930-5333-41D4-8035-D052F056BD65}.Release|Any CPU.ActiveCfg = Release|Any CPU + {29FEF930-5333-41D4-8035-D052F056BD65}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {EC808DD8-B0F5-48FC-AE87-05163C7BA6C4} + EndGlobalSection +EndGlobal diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Program.cs b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Program.cs new file mode 100644 index 000000000..a3cc00b7e --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Program.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// +// Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.3.0 + +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; + +namespace Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot +{ + public class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + public static IWebHostBuilder CreateWebHostBuilder(string[] args) => + WebHost.CreateDefaultBuilder(args) + .UseStartup(); + } +} diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/README.md b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/README.md new file mode 100644 index 000000000..bce7f2e2c --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/README.md @@ -0,0 +1,106 @@ +# EchoBot using Facebook Adapter + +Bot Framework v4 echo bot using facebook Adapter sample. + +This bot has been created using [Bot Framework](https://dev.botframework.com), it shows how to create a simple echo bot that connects with Facebook to respond to messages. + +## Prerequisites + +- [.NET Core SDK](https://dotnet.microsoft.com/download) version 2.1 + + ```bash + # determine dotnet version + dotnet --version + ``` + +## To try this sample + +1 - Clone the repository + + ```bash + git clone https://github.com/Microsoft/botbuilder-dotnet.git + ``` + + 2 - In a terminal, navigate to `test/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.TestBot` + + 3 - Connect the bot with Facebook by following the instructions below. + +4 - Run the bot from a terminal or from Visual Studio, choose option A or B. + + A) From a terminal + + ```bash + # run the bot + dotnet run + ``` + + B) Or from Visual Studio + + - Launch Visual Studio + - File -> Open -> Project/Solution + - Navigate to `test/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.TestBot` folder + - Select `Microsoft.Bot.Builder.Adapters.Facebook.TestBot.csproj` file + - Press F5 to run the project + + +### Connect the bot with Facebook + +1 - Create a Facebook Account for Developers (https://developers.facebook.com/). + +2 - Create a new App. Give a name to the app and click Create App ID button. + +3 - In the Dashboard go to Add a Product and select Messenger by clicking on Set Up button + + A) In the Access Tokens section, select a Facebook Page or create a new one. This is the page where the bot will be tested later. + + B) After selecting the page, the permissions should be edited, click on the button "Add or Remove Pages", select the page just created as option and continue. + + C) A Page Access Token is generated. Copy it, it will be needed to connect the adapter. + +4 - Get the app credentials. Go to Settings, Basic and copy the App Secret. + +5 - Set the tokens in appsettings.json file: + + - VerifyToken (create one. It will be used to validate received messages) + - AppSecret (the one obtained in step 4) + - AccessToken (the one obtained in step 3.c) + +6 - Using a tunneling tool like [Ngrok](https://ngrok.com/download), expose the bot's endpoint. + +7 - Go back to the Facebook for Developers page and click on Messenger, Settings. + In the Webhooks section, click on Subscribe To Events button. + + A) Complete the Callback URL with the ngrok https URL adding '/api/messages'. + Fill in the Verify Token with the one setted on your bot. + Subscribe to the following events: messages, messaging_postbacks, messaging_optins, messaging_deliveries + + B) Click Verify and Save button. + +8 - Subscribe the webhook to the Page. + +9 - Go to the Page and click Add a Button. + + A) Select a Send Message button. + + B) Select Messenger. + + C) And click Finish button. + +10 - Finally, click on the button created and test your bot! + +## Deploy the bot to Azure + +To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete list of deployment instructions. + +## Further reading + +- [Bot Framework Documentation](https://docs.botframework.com) +- [Bot Basics](https://docs.microsoft.com/azure/bot-service/bot-builder-basics?view=azure-bot-service-4.0) +- [Activity processing](https://docs.microsoft.com/en-us/azure/bot-service/bot-builder-concept-activity-processing?view=azure-bot-service-4.0) +- [Azure Bot Service Introduction](https://docs.microsoft.com/azure/bot-service/bot-service-overview-introduction?view=azure-bot-service-4.0) +- [Azure Bot Service Documentation](https://docs.microsoft.com/azure/bot-service/?view=azure-bot-service-4.0) +- [.NET Core CLI tools](https://docs.microsoft.com/en-us/dotnet/core/tools/?tabs=netcore2x) +- [Azure CLI](https://docs.microsoft.com/cli/azure/?view=azure-cli-latest) +- [Azure Portal](https://portal.azure.com) +- [Language Understanding using LUIS](https://docs.microsoft.com/en-us/azure/cognitive-services/luis/) +- [Channels and Bot Connector Service](https://docs.microsoft.com/en-us/azure/bot-service/bot-concepts?view=azure-bot-service-4.0) diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Resources/ButtonTemplatePayload.json b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Resources/ButtonTemplatePayload.json new file mode 100644 index 000000000..681a5bcd6 --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Resources/ButtonTemplatePayload.json @@ -0,0 +1,21 @@ +{ + "template_type": "button", + "text": "What do you want to do next?", + "buttons": [ + { + "type": "web_url", + "url": "https://www.messenger.com", + "title": "Visit Messenger" + }, + { + "type": "postback", + "title": "Say Hello", + "payload": "Hello button" + }, + { + "type": "postback", + "title": "Say Goodbye", + "payload": "Goodbye button" + } + ] +} \ No newline at end of file diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Resources/GenericTemplatePayload.json b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Resources/GenericTemplatePayload.json new file mode 100644 index 000000000..6b48d7627 --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Resources/GenericTemplatePayload.json @@ -0,0 +1,27 @@ +{ + "template_type": "generic", + "elements": [ + { + "title": "Welcome to BotFramework!", + "image_url": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQtB3AwMUeNoq4gUBGe6Ocj8kyh3bXa9ZbV7u1fVKQoyKFHdkqU", + "subtitle": "Know more about Bots.", + "default_action": { + "type": "web_url", + "url": "https://dev.botframework.com/", + "webview_height_ratio": "compact" + }, + "buttons": [ + { + "type": "web_url", + "url": "https://dev.botframework.com/", + "title": "View Website" + }, + { + "type": "postback", + "title": "Start Chatting", + "payload": "Chatting" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Resources/HandoverTemplatePayload.json b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Resources/HandoverTemplatePayload.json new file mode 100644 index 000000000..e85a5278a --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Resources/HandoverTemplatePayload.json @@ -0,0 +1,11 @@ +{ + "template_type": "button", + "text": "You can contact with a live agent.", + "buttons": [ + { + "type": "postback", + "title": "Contact", + "payload": "Handover" + } + ] +} \ No newline at end of file diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Resources/MediaTemplatePayload.json b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Resources/MediaTemplatePayload.json new file mode 100644 index 000000000..83a3648f9 --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Resources/MediaTemplatePayload.json @@ -0,0 +1,9 @@ +{ + "template_type": "media", + "elements": [ + { + "media_type": "video", + "url": "https://www.facebook.com/MicrosoftLatam/videos/724316691373804/" + } + ] +} \ No newline at end of file diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Startup.cs b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Startup.cs new file mode 100644 index 000000000..165183b46 --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/Startup.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// +// Generated with Bot Builder V4 SDK Template for Visual Studio EchoBot v4.3.0 + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot.Bots; +using Microsoft.Bot.Builder.Integration.AspNet.Core; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); + + // Create the Bot Framework Facebook Adapter. + services.AddSingleton(); + + // Create the bot as a transient. In this case the ASP Controller is expecting an IBot. + services.AddTransient(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IHostingEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseHsts(); + } + + app.UseDefaultFiles(); + app.UseStaticFiles(); + + // app.UseHttpsRedirection(); + app.UseMvc(); + } + } +} diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/appsettings.json b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/appsettings.json new file mode 100644 index 000000000..f1025d4fa --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/appsettings.json @@ -0,0 +1,5 @@ +{ + "FacebookVerifyToken": "", + "FacebookAppSecret": "", + "FacebookAccessToken": "" +} diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/wwwroot/default.htm b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/wwwroot/default.htm new file mode 100644 index 000000000..d4e6c2c71 --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.SecondaryTestBot/wwwroot/default.htm @@ -0,0 +1,420 @@ + + + + + + + Microsoft.Bot.Builder.Facebook.Sample + + + + + +
+
+
+
Microsoft.Bot.Builder.Facebook.Sample Bot
+
+
+
+
+
Your bot is ready!
+
You can test your bot in the Bot Framework Emulator
+ by connecting to http://localhost:3978/api/messages.
+ +
Visit Azure + Bot Service to register your bot and add it to
+ various channels. The bot's endpoint URL typically looks + like this:
+
https://your_bots_hostname/api/messages
+
+
+
+
+ +
+ + + \ No newline at end of file diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.TestBot/Bots/EchoBot.cs b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.TestBot/Bots/EchoBot.cs index ba47f1eb0..7f2d96ed6 100644 --- a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.TestBot/Bots/EchoBot.cs +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.TestBot/Bots/EchoBot.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.Bot.Builder.Adapters.Facebook.FacebookEvents.Handover; using Microsoft.Bot.Schema; using Newtonsoft.Json; @@ -14,6 +15,8 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook.TestBot.Bots { public class EchoBot : ActivityHandler { + private const string PageInboxId = "263902037430900"; + protected override async Task OnMembersAddedAsync(IList membersAdded, ITurnContext turnContext, CancellationToken cancellationToken) { var activity = MessageFactory.Text("Hello and Welcome!"); @@ -22,7 +25,19 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook.TestBot.Bots protected override async Task OnMessageActivityAsync(ITurnContext turnContext, CancellationToken cancellationToken) { - if (turnContext.Activity.Attachments != null) + if (turnContext.Activity.GetChannelData().IsStandby) + { + if ((turnContext.Activity as Activity)?.Text == "Little") + { + var activity = MessageFactory.Text("Primary Bot taking back control..."); + activity.Type = ActivityTypes.Event; + + //Action + (activity as IEventActivity).Name = "take_thread_control"; + await turnContext.SendActivityAsync(activity, cancellationToken); + } + } + else if (turnContext.Activity.Attachments != null) { foreach (var attachment in turnContext.Activity.Attachments) { @@ -60,6 +75,38 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook.TestBot.Bots case "Chatting": activity = MessageFactory.Text("Hello! How can I help you?"); break; + case "handover template": + activity = MessageFactory.Attachment(CreateTemplateAttachment(Directory.GetCurrentDirectory() + @"/Resources/HandoverTemplatePayload.json")); + break; + case "Handover": + activity = MessageFactory.Text("Redirecting..."); + activity.Type = ActivityTypes.Event; + (activity as IEventActivity).Name = "pass_thread_control"; + (activity as IEventActivity).Value = "inbox"; + break; + case "bot template": + activity = MessageFactory.Attachment(CreateTemplateAttachment(Directory.GetCurrentDirectory() + @"/Resources/HandoverBotsTemplatePayload.json")); + break; + case "SecondaryBot": + activity = MessageFactory.Text("Redirecting to the secondary bot..."); + activity.Type = ActivityTypes.Event; + + //Action + (activity as IEventActivity).Name = "pass_thread_control"; + + //AppId + (activity as IEventActivity).Value = ""; + break; + case "TakeControl": + activity = MessageFactory.Text("Primary Bot Taking control..."); + activity.Type = ActivityTypes.Event; + + //Action + (activity as IEventActivity).Name = "take_thread_control"; + break; + case "OtherBot": + activity = MessageFactory.Text($"Secondary bot is requesting me the thread control. Passing thread control!"); + break; default: activity = MessageFactory.Text($"Echo: {turnContext.Activity.Text}"); break; @@ -73,11 +120,34 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook.TestBot.Bots { if (turnContext.Activity.Value != null) { - var inputs = (Dictionary)turnContext.Activity.Value; - var name = inputs["Name"]; + var metadata = (turnContext.Activity.Value as FacebookThreadControl).Metadata; - var activity = MessageFactory.Text($"How are you doing {name}?"); - await turnContext.SendActivityAsync(activity, cancellationToken); + if (metadata == null) + { + var requester = (turnContext.Activity.Value as FacebookRequestThreadControl).RequestedOwnerAppId; + + if (requester == PageInboxId) + { + var activity = MessageFactory.Text($"The Inbox is requesting me the thread control. Passing thread control!"); + activity.Type = ActivityTypes.Event; + (activity as IEventActivity).Name = "pass_thread_control"; + (activity as IEventActivity).Value = "inbox"; + await turnContext.SendActivityAsync(activity, cancellationToken); + } + } + else if (metadata.Equals("Request thread control to the primary receiver")) + { + var activity = new Activity(); + activity.Type = ActivityTypes.Event; + (activity as IEventActivity).Name = "pass_thread_control"; + (activity as IEventActivity).Value = ""; + await turnContext.SendActivityAsync(activity, cancellationToken); + } + else if (metadata.Equals("Pass thread control") || metadata.Equals("Pass thread control from Page Inbox")) + { + var activity = MessageFactory.Text("Hello Again Human, I'm the bot"); + await turnContext.SendActivityAsync(activity, cancellationToken); + } } } diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.TestBot/Resources/HandoverBotsTemplatePayload.json b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.TestBot/Resources/HandoverBotsTemplatePayload.json new file mode 100644 index 000000000..8f87f2116 --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.TestBot/Resources/HandoverBotsTemplatePayload.json @@ -0,0 +1,11 @@ +{ + "template_type": "button", + "text": "You can contact with a secondary bot.", + "buttons": [ + { + "type": "postback", + "title": "Contact", + "payload": "SecondaryBot" + } + ] +} \ No newline at end of file diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.TestBot/Resources/HandoverTemplatePayload.json b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.TestBot/Resources/HandoverTemplatePayload.json new file mode 100644 index 000000000..e85a5278a --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.TestBot/Resources/HandoverTemplatePayload.json @@ -0,0 +1,11 @@ +{ + "template_type": "button", + "text": "You can contact with a live agent.", + "buttons": [ + { + "type": "postback", + "title": "Contact", + "payload": "Handover" + } + ] +} \ No newline at end of file diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/FacebookAdapterTests.cs b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/FacebookAdapterTests.cs index 1296b3857..4f6cc8999 100644 --- a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/FacebookAdapterTests.cs +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/FacebookAdapterTests.cs @@ -226,24 +226,101 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook.Tests } [Fact] - public async void SendActivitiesAsyncShouldFailWithActivityTypeNotMessage() + public async void SendActivitiesAsyncShouldPostToFacebookOnPassThreadControl() { - var facebookAdapter = new FacebookAdapter(new Mock(_testOptions).Object); + const string testResponse = "Test Response"; + var facebookClientWrapper = new Mock(_testOptions); + var facebookAdapter = new FacebookAdapter(facebookClientWrapper.Object); var activity = new Activity { Type = ActivityTypes.Event, + Text = "Test text", + Name = "pass_thread_control", + Conversation = new ConversationAccount() + { + Id = "Test id", + }, + ChannelData = new FacebookMessage("recipientId", new Message(), "messagingtype"), }; Activity[] activities = { activity }; + ResourceResponse[] responses = null; + + facebookClientWrapper.Setup(api => api.SendMessageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(testResponse)); + facebookClientWrapper.Setup(api => api.PassThreadControlAsync(It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); using (var turnContext = new TurnContext(facebookAdapter, activity)) { - await Assert.ThrowsAsync(async () => - { - await facebookAdapter.SendActivitiesAsync(turnContext, activities, default); - }); + responses = await facebookAdapter.SendActivitiesAsync(turnContext, activities, default); } + + Assert.Equal(testResponse, responses[0].Id); + facebookClientWrapper.Verify(api => api.PassThreadControlAsync(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); } + [Fact] + public async void SendActivitiesAsyncShouldPostToFacebookOnTakeThreadControl() + { + const string testResponse = "Test Response"; + var facebookClientWrapper = new Mock(_testOptions); + var facebookAdapter = new FacebookAdapter(facebookClientWrapper.Object); + var activity = new Activity + { + Type = ActivityTypes.Event, + Text = "Test text", + Name = "take_thread_control", + Conversation = new ConversationAccount() + { + Id = "Test id", + }, + ChannelData = new FacebookMessage("recipientId", new Message(), "messagingtype"), + }; + Activity[] activities = { activity }; + ResourceResponse[] responses = null; + + facebookClientWrapper.Setup(api => api.SendMessageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(testResponse)); + facebookClientWrapper.Setup(api => api.TakeThreadControlAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); + + using (var turnContext = new TurnContext(facebookAdapter, activity)) + { + responses = await facebookAdapter.SendActivitiesAsync(turnContext, activities, default); + } + + Assert.Equal(testResponse, responses[0].Id); + facebookClientWrapper.Verify(api => api.TakeThreadControlAsync(It.IsAny(), It.IsAny()), Times.Once); + } + + [Fact] + public async void SendActivitiesAsyncShouldPostToFacebookOnRequestThreadControl() + { + const string testResponse = "Test Response"; + var facebookClientWrapper = new Mock(_testOptions); + var facebookAdapter = new FacebookAdapter(facebookClientWrapper.Object); + var activity = new Activity + { + Type = ActivityTypes.Event, + Text = "Test text", + Name = "request_thread_control", + Conversation = new ConversationAccount() + { + Id = "Test id", + }, + ChannelData = new FacebookMessage("recipientId", new Message(), "messagingtype"), + }; + Activity[] activities = { activity }; + ResourceResponse[] responses = null; + + facebookClientWrapper.Setup(api => api.SendMessageAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Task.FromResult(testResponse)); + facebookClientWrapper.Setup(api => api.RequestThreadControlAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); + + using (var turnContext = new TurnContext(facebookAdapter, activity)) + { + responses = await facebookAdapter.SendActivitiesAsync(turnContext, activities, default); + } + + Assert.Equal(testResponse, responses[0].Id); + facebookClientWrapper.Verify(api => api.RequestThreadControlAsync(It.IsAny(), It.IsAny()), Times.Once); + } + [Fact] public async Task UpdateActivityAsyncShouldThrowNotImplementedException() { diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/FacebookHelperTests.cs b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/FacebookHelperTests.cs index 997fb60eb..273800052 100644 --- a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/FacebookHelperTests.cs +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/FacebookHelperTests.cs @@ -9,6 +9,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.Bot.Builder.Adapters.Facebook.FacebookEvents; using Microsoft.Bot.Schema; using Moq; using Newtonsoft.Json; @@ -102,6 +103,57 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook.Tests Assert.Equal(facebookMessage.PostBack.Payload, activity.Text); } + [Fact] + public void ProcessSingleMessageShouldReturnActivityWithPassThreadControlRequest() + { + var facebookMessageJson = File.ReadAllText(Directory.GetCurrentDirectory() + @"/Files/PayloadWithPassThreadControl.json"); + var facebookResponse = JsonConvert.DeserializeObject(facebookMessageJson); + + var payload = new List(); + + payload = facebookResponse.Entry[0].Messaging; + + var activity = FacebookHelper.ProcessSingleMessage(payload[0]); + Assert.Equal(facebookResponse.Entry[0].Messaging[0].Recipient.Id, activity.Recipient.Id); + Assert.Equal(facebookResponse.Entry[0].Messaging[0].Sender.Id, activity.Conversation.Id); + Assert.Equal(facebookResponse.Entry[0].Messaging[0].Sender.Id, activity.Conversation.Id); + Assert.Equal(facebookResponse.Entry[0].Messaging[0].PassThreadControl, activity.Value); + } + + [Fact] + public void ProcessSingleMessageShouldReturnActivityWithRequestThreadControlRequest() + { + var facebookMessageJson = File.ReadAllText(Directory.GetCurrentDirectory() + @"/Files/PayloadWithRequestThreadControl.json"); + var facebookResponse = JsonConvert.DeserializeObject(facebookMessageJson); + + var payload = new List(); + + payload = facebookResponse.Entry[0].Messaging; + + var activity = FacebookHelper.ProcessSingleMessage(payload[0]); + Assert.Equal(facebookResponse.Entry[0].Messaging[0].Recipient.Id, activity.Recipient.Id); + Assert.Equal(facebookResponse.Entry[0].Messaging[0].Sender.Id, activity.Conversation.Id); + Assert.Equal(facebookResponse.Entry[0].Messaging[0].Sender.Id, activity.Conversation.Id); + Assert.Equal(facebookResponse.Entry[0].Messaging[0].RequestThreadControl, activity.Value); + } + + [Fact] + public void ProcessSingleMessageShouldReturnActivityWithTakeThreadControlRequest() + { + var facebookMessageJson = File.ReadAllText(Directory.GetCurrentDirectory() + @"/Files/PayloadWithTakeThreadControl.json"); + var facebookResponse = JsonConvert.DeserializeObject(facebookMessageJson); + + var payload = new List(); + + payload = facebookResponse.Entry[0].Messaging; + + var activity = FacebookHelper.ProcessSingleMessage(payload[0]); + Assert.Equal(facebookResponse.Entry[0].Messaging[0].Recipient.Id, activity.Recipient.Id); + Assert.Equal(facebookResponse.Entry[0].Messaging[0].Sender.Id, activity.Conversation.Id); + Assert.Equal(facebookResponse.Entry[0].Messaging[0].Sender.Id, activity.Conversation.Id); + Assert.Equal(facebookResponse.Entry[0].Messaging[0].TakeThreadControl, activity.Value); + } + [Fact] public async Task WriteAsyncShouldFailWithNullResponse() { diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/FacebookWrapperTests.cs b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/FacebookWrapperTests.cs index 91d43342f..1928db8e2 100644 --- a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/FacebookWrapperTests.cs +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/FacebookWrapperTests.cs @@ -160,5 +160,53 @@ namespace Microsoft.Bot.Builder.Adapters.Facebook.Tests await Assert.ThrowsAsync(async () => { await facebookClientWrapper.VerifyWebhookAsync(httpRequest.Object, null, default); }); } + + [Fact] + public async void PassThreadControlAsyncShouldThrowExceptionWithNullTargetAppId() + { + var facebookClientWrapper = new FacebookClientWrapper(_testOptions); + + await Assert.ThrowsAsync(async () => { await facebookClientWrapper.PassThreadControlAsync(null, "fakeUserId", "Test Pass Thread Control"); }); + } + + [Fact] + public async void PassThreadControlAsyncShouldThrowExceptionWithNullUserId() + { + var facebookClientWrapper = new FacebookClientWrapper(_testOptions); + + await Assert.ThrowsAsync(async () => { await facebookClientWrapper.PassThreadControlAsync("fakeAppId", null, "Test Pass Thread Control"); }); + } + + [Fact] + public async void RequestThreadControlAsyncShouldThrowExceptionWithNullUserId() + { + var facebookClientWrapper = new FacebookClientWrapper(_testOptions); + + await Assert.ThrowsAsync(async () => { await facebookClientWrapper.RequestThreadControlAsync(null, "Test Pass Thread Control"); }); + } + + [Fact] + public async void TakeThreadControlAsyncShouldThrowExceptionWithNullUserId() + { + var facebookClientWrapper = new FacebookClientWrapper(_testOptions); + + await Assert.ThrowsAsync(async () => { await facebookClientWrapper.TakeThreadControlAsync(null, "Test Pass Thread Control"); }); + } + + [Fact] + public async void PostToFacebookApiAsyncShouldThrowExceptionWithNullPostType() + { + var facebookClientWrapper = new FacebookClientWrapper(_testOptions); + + await Assert.ThrowsAsync(async () => { await facebookClientWrapper.PostToFacebookApiAsync(null, "fakeContent"); }); + } + + [Fact] + public async void PostToFacebookApiAsyncShouldThrowExceptionWithNullContent() + { + var facebookClientWrapper = new FacebookClientWrapper(_testOptions); + + await Assert.ThrowsAsync(async () => { await facebookClientWrapper.PostToFacebookApiAsync("fakePostType", null); }); + } } } diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/Files/PayloadWithPassThreadControl.json b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/Files/PayloadWithPassThreadControl.json new file mode 100644 index 000000000..f790ac9d9 --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/Files/PayloadWithPassThreadControl.json @@ -0,0 +1,24 @@ +{ + "object": "page", + "entry": [ + { + "id": "fakeId", + "time": 1572630578783, + "messaging": [ + { + "recipient": { + "id": "fakeId" + }, + "timestamp": 1572630578783, + "sender": { + "id": "fakeId" + }, + "pass_thread_control": { + "new_owner_app_id": 1234567890, + "metadata": "Pass thread control" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/Files/PayloadWithRequestThreadControl.json b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/Files/PayloadWithRequestThreadControl.json new file mode 100644 index 000000000..118c27d12 --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/Files/PayloadWithRequestThreadControl.json @@ -0,0 +1,24 @@ +{ + "object": "page", + "entry": [ + { + "id": "fakeId", + "time": 1572630401588, + "messaging": [ + { + "recipient": { + "id": "fakeId" + }, + "timestamp": 1572630401588, + "sender": { + "id": "fakeId" + }, + "request_thread_control": { + "requested_owner_app_id": 1234567890, + "metadata": null + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/Files/PayloadWithTakeThreadControl.json b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/Files/PayloadWithTakeThreadControl.json new file mode 100644 index 000000000..10503e5ad --- /dev/null +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/Files/PayloadWithTakeThreadControl.json @@ -0,0 +1,24 @@ +{ + "object": "page", + "entry": [ + { + "id": "fakeId", + "time": 1572630386105, + "messaging": [ + { + "recipient": { + "id": "fakeId" + }, + "timestamp": 1572630386105, + "sender": { + "id": "fakeId" + }, + "take_thread_control": { + "previous_owner_app_id": 1234567890, + "metadata": "Take thread control from a secondary receiver" + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/Microsoft.Bot.Builder.Adapters.Facebook.Tests.csproj b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/Microsoft.Bot.Builder.Adapters.Facebook.Tests.csproj index 57112b855..439869621 100644 --- a/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/Microsoft.Bot.Builder.Adapters.Facebook.Tests.csproj +++ b/tests/Adapters/Microsoft.Bot.Builder.Adapters.Facebook.Tests/Microsoft.Bot.Builder.Adapters.Facebook.Tests.csproj @@ -33,6 +33,15 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + PreserveNewest