diff --git a/libraries/Microsoft.Bot.Builder/Teams/TeamsActivityHandler.cs b/libraries/Microsoft.Bot.Builder/Teams/TeamsActivityHandler.cs index 1701b7c793..b1f501a1c4 100644 --- a/libraries/Microsoft.Bot.Builder/Teams/TeamsActivityHandler.cs +++ b/libraries/Microsoft.Bot.Builder/Teams/TeamsActivityHandler.cs @@ -85,10 +85,10 @@ protected override async Task OnInvokeActivityAsync(ITurnContext case "task/submit": return CreateInvokeResponse(await OnTeamsTaskModuleSubmitAsync(turnContext, SafeCast(turnContext.Activity.Value), cancellationToken).ConfigureAwait(false)); - + case "tab/fetch": return CreateInvokeResponse(await OnTeamsTabFetchAsync(turnContext, SafeCast(turnContext.Activity.Value), cancellationToken).ConfigureAwait(false)); - + case "tab/submit": return CreateInvokeResponse(await OnTeamsTabSubmitAsync(turnContext, SafeCast(turnContext.Activity.Value), cancellationToken).ConfigureAwait(false)); @@ -98,6 +98,13 @@ protected override async Task OnInvokeActivityAsync(ITurnContext case "config/submit": return CreateInvokeResponse(await OnTeamsConfigSubmitAsync(turnContext, turnContext.Activity.Value as JObject, cancellationToken).ConfigureAwait(false)); + case "message/submitAction": + await OnTeamsMessageSubmitActionAsync(turnContext, SafeCast(turnContext.Activity.Value), cancellationToken).ConfigureAwait(false); + return CreateInvokeResponse(); + + case "message/fetchTask": + return CreateInvokeResponse(await OnTeamsMessageFetchTaskAsync(turnContext, cancellationToken).ConfigureAwait(false)); + default: return await base.OnInvokeActivityAsync(turnContext, cancellationToken).ConfigureAwait(false); } @@ -686,7 +693,7 @@ protected virtual Task OnTeamsChannelRenamedAsync(ChannelInfo channelInfo, TeamI { return Task.CompletedTask; } - + /// /// Invoked when a Channel Restored event activity is received from the connector. /// Channel Restored correspond to the user restoring a previously deleted channel. @@ -994,6 +1001,69 @@ protected virtual Task OnTeamsMessageSoftDeleteAsync(ITurnContext + /// Invoked when a feedback loop activity is received. + /// + /// A strongly-typed context object for this turn. + /// A strongly-typed feedback loop object for this turn. + /// A cancellation token that can be used by other objects + /// or threads to receive notice of cancellation. + /// A task that represents the work queued to execute. + protected virtual Task OnTeamsMessageSubmitActionAsync(ITurnContext turnContext, FeedbackResponse feedback, CancellationToken cancellationToken) + { + throw new InvokeResponseException(HttpStatusCode.NotImplemented); + } + + /// + /// Invoked when a custom feedback loop activity is received. + /// The returned information is a instance, + /// that Teams will use to show either an AdaptiveCard or website in the "feedback window form". + /// + ///

+ /// Example of a valid Adaptive Card payload: + /// + /// const taskModuleReturn = { + ///   task: { + ///     type: 'continue', + ///     value: { + ///       card: [Object], // Should contain valid Adaptive Card Payload + ///       height: 200, + ///       width: 400, + ///       title: 'Test Task Module Title with AC' + ///     } + ///   } + /// }; + /// return { + /// status: 200, + /// body: taskModuleReturn + /// }; + /// + /// Example of a valid website payload: + /// + /// const taskModuleReturn = { + ///   task: { + ///     type: 'continue', + ///     value: { + ///       url: "https://bing.com", // Should contain valid URL with the valid domain listed under App Manifest + ///     } + ///   } + /// }; + /// return { + /// status: 200, + /// body: taskModuleReturn + /// }; + /// + ///
+ ///
+ /// A strongly-typed context object for this turn. + /// A cancellation token that can be used by other objects + /// or threads to receive notice of cancellation. + /// A task that represents a instance, containing the necessary metadata with either an AdaptiveCard or a website url information. + protected virtual Task OnTeamsMessageFetchTaskAsync(ITurnContext turnContext, CancellationToken cancellationToken) + { + throw new InvokeResponseException(HttpStatusCode.NotImplemented); + } + /// /// Safely casts an object to an object of type . /// diff --git a/libraries/Microsoft.Bot.Schema/Teams/FeedbackInfo.cs b/libraries/Microsoft.Bot.Schema/Teams/FeedbackInfo.cs new file mode 100644 index 0000000000..71c3eff361 --- /dev/null +++ b/libraries/Microsoft.Bot.Schema/Teams/FeedbackInfo.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Bot.Schema.Teams +{ + using Newtonsoft.Json; + + /// + /// Describes feedback loop information. + /// + public partial class FeedbackInfo + { + /// + /// Initializes a new instance of the class. + /// + public FeedbackInfo() + { + CustomInit(); + } + + /// + /// Initializes a new instance of the class. + /// + /// Unique identifier representing a team. + public FeedbackInfo(string type = FeedbackInfoTypes.Default) + { + Type = type; + CustomInit(); + } + + /// + /// Gets or sets the feedback loop type. Possible values include: 'default', 'custom'. + /// + /// + /// The feedback loop type (see ). + /// + [JsonProperty(PropertyName = "type")] + public string Type { get; set; } + + partial void CustomInit(); + } +} diff --git a/libraries/Microsoft.Bot.Schema/Teams/FeedbackInfoTypes.cs b/libraries/Microsoft.Bot.Schema/Teams/FeedbackInfoTypes.cs new file mode 100644 index 0000000000..996aebb146 --- /dev/null +++ b/libraries/Microsoft.Bot.Schema/Teams/FeedbackInfoTypes.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Bot.Schema.Teams +{ + /// + /// Defines feedback loop type. Depending on the type, the feedback window will have a different structure. + /// + public static class FeedbackInfoTypes + { + /// + /// The type value for default feedback window form. + /// + public const string Default = "default"; + + /// + /// The type value for custom feedback window, can be either an AdaptiveCard or website. + /// + public const string Custom = "custom"; + } +} diff --git a/libraries/Microsoft.Bot.Schema/Teams/FeedbackResponse.cs b/libraries/Microsoft.Bot.Schema/Teams/FeedbackResponse.cs new file mode 100644 index 0000000000..196d87ce6b --- /dev/null +++ b/libraries/Microsoft.Bot.Schema/Teams/FeedbackResponse.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Bot.Schema.Teams +{ + using Newtonsoft.Json; + + /// + /// Envelope for Feedback Response. + /// + public partial class FeedbackResponse + { + /// + /// Initializes a new instance of the class. + /// + public FeedbackResponse() + { + CustomInit(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the action. + /// The value of the action. + /// The ID of the message to which this message is a reply. + public FeedbackResponse(string actionName = default, FeedbackResponseActionValue actionValue = default, string replyToId = default) + { + ActionName = actionName; + ActionValue = actionValue; + ReplyToId = replyToId; + CustomInit(); + } + + /// + /// Gets or sets the action name. + /// + /// Name of the action. + public string ActionName { get; set; } = "feedback"; + + /// + /// Gets or sets the response for the action value. + /// + /// The action value that contains the feedback reaction and message. + [JsonProperty(PropertyName = "actionValue")] + public FeedbackResponseActionValue ActionValue { get; set; } + + /// + /// Gets or sets the ID of the message to which this message is a reply. + /// + /// Value of the ID to reply. + [JsonProperty(PropertyName = "replyToId")] + public string ReplyToId { get; set; } + + partial void CustomInit(); + } +} diff --git a/libraries/Microsoft.Bot.Schema/Teams/FeedbackResponseActionValue.cs b/libraries/Microsoft.Bot.Schema/Teams/FeedbackResponseActionValue.cs new file mode 100644 index 0000000000..34d52ba0f1 --- /dev/null +++ b/libraries/Microsoft.Bot.Schema/Teams/FeedbackResponseActionValue.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Bot.Schema.Teams +{ + using Newtonsoft.Json; + + /// + /// Envelope for Feedback ActionValue Response. + /// + public partial class FeedbackResponseActionValue + { + /// + /// Initializes a new instance of the class. + /// + public FeedbackResponseActionValue() + { + CustomInit(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The reaction of the feedback. + /// The feedback content. + public FeedbackResponseActionValue(string reaction = default, string feedback = default) + { + Reaction = reaction; + Feedback = feedback; + CustomInit(); + } + + /// + /// Gets or sets the reaction, either "like" or "dislike". + /// + /// val. + [JsonProperty(PropertyName = "reaction")] + public string Reaction { get; set; } + + /// + /// Gets or sets the feedback content provided by the user when prompted with "What did you like/dislike?". + /// + /// val. + [JsonProperty(PropertyName = "feedback")] + public string Feedback { get; set; } + + partial void CustomInit(); + } +} diff --git a/libraries/Microsoft.Bot.Schema/Teams/TeamsChannelData.cs b/libraries/Microsoft.Bot.Schema/Teams/TeamsChannelData.cs index f8aef4a470..293c2713af 100644 --- a/libraries/Microsoft.Bot.Schema/Teams/TeamsChannelData.cs +++ b/libraries/Microsoft.Bot.Schema/Teams/TeamsChannelData.cs @@ -107,6 +107,13 @@ public TeamsChannelData(ChannelInfo channel = default, string eventType = defaul [JsonProperty(PropertyName = "settings")] public TeamsChannelDataSettings Settings { get; set; } + /// + /// Gets or sets information about custom feedback buttons. + /// + /// The for this . + [JsonProperty(PropertyName = "feedbackLoop")] + public FeedbackInfo FeedbackLoop { get; set; } + /// /// Gets the OnBehalfOf list for user attribution. /// diff --git a/tests/Microsoft.Bot.Builder.Tests/Teams/TeamsActivityHandlerNotImplementedTests.cs b/tests/Microsoft.Bot.Builder.Tests/Teams/TeamsActivityHandlerNotImplementedTests.cs index 2d3650573a..2cc60cf9ae 100644 --- a/tests/Microsoft.Bot.Builder.Tests/Teams/TeamsActivityHandlerNotImplementedTests.cs +++ b/tests/Microsoft.Bot.Builder.Tests/Teams/TeamsActivityHandlerNotImplementedTests.cs @@ -688,6 +688,64 @@ void CaptureSend(Activity[] arg) Assert.Equal(200, ((InvokeResponse)activitiesToSend[0].Value).Status); } + [Fact] + public async Task TestMessageSubmitAction() + { + // Arrange + var activity = new Activity + { + Type = ActivityTypes.Invoke, + Name = "message/submitAction" + }; + + Activity[] activitiesToSend = null; + void CaptureSend(Activity[] arg) + { + activitiesToSend = arg; + } + + var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); + + // Act + var bot = new TestActivityHandler(); + await ((IBot)bot).OnTurnAsync(turnContext); + + // Assert + Assert.NotNull(activitiesToSend); + Assert.Single(activitiesToSend); + Assert.IsType(activitiesToSend[0].Value); + Assert.Equal(200, ((InvokeResponse)activitiesToSend[0].Value).Status); + } + + [Fact] + public async Task TestMessageFetchTask() + { + // Arrange + var activity = new Activity + { + Type = ActivityTypes.Invoke, + Name = "message/fetchTask" + }; + + Activity[] activitiesToSend = null; + void CaptureSend(Activity[] arg) + { + activitiesToSend = arg; + } + + var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); + + // Act + var bot = new TestActivityHandler(); + await ((IBot)bot).OnTurnAsync(turnContext); + + // Assert + Assert.NotNull(activitiesToSend); + Assert.Single(activitiesToSend); + Assert.IsType(activitiesToSend[0].Value); + Assert.Equal(501, ((InvokeResponse)activitiesToSend[0].Value).Status); + } + private class TestActivityHandler : TeamsActivityHandler { } diff --git a/tests/Microsoft.Bot.Builder.Tests/Teams/TeamsActivityHandlerTests.cs b/tests/Microsoft.Bot.Builder.Tests/Teams/TeamsActivityHandlerTests.cs index 4cc3263efb..1bbf77cf9e 100644 --- a/tests/Microsoft.Bot.Builder.Tests/Teams/TeamsActivityHandlerTests.cs +++ b/tests/Microsoft.Bot.Builder.Tests/Teams/TeamsActivityHandlerTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net; using System.Net.Http; using System.Reflection; @@ -1512,6 +1513,78 @@ public async Task TestMessageDeleteActivityTeams_NoChannelData() Assert.Equal("OnMessageDeleteActivityAsync", bot.Record[0]); } + [Fact] + public async Task TestMessageSubmitAction() + { + // Arrange + var activity = new Activity + { + Type = ActivityTypes.Invoke, + Name = "message/submitAction", + Value = JObject.FromObject(new FeedbackResponse + { + ReplyToId = "replyId", + ActionValue = new FeedbackResponseActionValue + { + Reaction = "like", + Feedback = "feedback message" + } + }), + }; + + List activitiesToSend = new List(); + void CaptureSend(Activity[] arg) + { + activitiesToSend.AddRange(arg.ToList()); + } + + var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); + + // Act + var bot = new TestActivityHandler(); + await ((IBot)bot).OnTurnAsync(turnContext); + + // Assert + Assert.Equal(2, bot.Record.Count); + Assert.Equal("OnInvokeActivityAsync", bot.Record[0]); + Assert.Equal("OnTeamsMessageSubmitActionAsync", bot.Record[1]); + Assert.NotNull(activitiesToSend); + Assert.Equal(2, activitiesToSend.Count); + Assert.Equal(JsonConvert.SerializeObject(activity.Value), activitiesToSend[0].Text); + Assert.IsType(activitiesToSend[1].Value); + Assert.Equal(200, ((InvokeResponse)activitiesToSend[1].Value).Status); + } + + [Fact] + public async Task TestMessageFetchTask() + { + // Arrange + var activity = new Activity + { + Type = ActivityTypes.Invoke, + Name = "message/fetchTask" + }; + + Activity[] activitiesToSend = null; + void CaptureSend(Activity[] arg) + { + activitiesToSend = arg; + } + + var turnContext = new TurnContext(new SimpleAdapter(CaptureSend), activity); + + // Act + var bot = new TestActivityHandler(); + await ((IBot)bot).OnTurnAsync(turnContext); + + // Assert + Assert.NotNull(activitiesToSend); + Assert.Single(activitiesToSend); + Assert.IsType(activitiesToSend[0].Value); + Assert.Equal(200, ((InvokeResponse)activitiesToSend[0].Value).Status); + Assert.Equal("http://testing", ((TaskModuleContinueResponse)((InvokeResponse)activitiesToSend[0].Value).Body).Value.Url); + } + private class NotImplementedAdapter : BotAdapter { public override Task DeleteActivityAsync(ITurnContext turnContext, ConversationReference reference, CancellationToken cancellationToken) @@ -1848,6 +1921,20 @@ protected override Task OnTeamsMessageSoftDeleteAsync(ITurnContext turnContext, FeedbackResponse feedback, CancellationToken cancellationToken) + { + Record.Add(MethodBase.GetCurrentMethod().Name); + return turnContext.SendActivityAsync(JsonConvert.SerializeObject(feedback)); + } + + protected override Task OnTeamsMessageFetchTaskAsync(ITurnContext turnContext, CancellationToken cancellationToken) + { + var info = new TaskModuleTaskInfo(url: "http://testing"); + var task = new TaskModuleContinueResponse(info); + Record.Add(MethodBase.GetCurrentMethod().Name); + return Task.FromResult(task); + } } private class RosterHttpMessageHandler : HttpMessageHandler diff --git a/tests/Microsoft.Bot.Schema.Tests/Teams/FeedbackInfoTests.cs b/tests/Microsoft.Bot.Schema.Tests/Teams/FeedbackInfoTests.cs new file mode 100644 index 0000000000..e270bd91a1 --- /dev/null +++ b/tests/Microsoft.Bot.Schema.Tests/Teams/FeedbackInfoTests.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Bot.Schema.Teams; +using Xunit; + +namespace Microsoft.Bot.Schema.Tests.Teams +{ + public class FeedbackInfoTests + { + [Fact] + public void FeedbackInfoInitsWithNoArgs() + { + var feedbackLoopInfo = new FeedbackInfo(); + + Assert.NotNull(feedbackLoopInfo); + Assert.IsType(feedbackLoopInfo); + Assert.Equal(FeedbackInfoTypes.Default, feedbackLoopInfo.Type); + } + + [Fact] + public void FeedbackInfoInitsDefault() + { + var feedbackLoopInfo = new FeedbackInfo(FeedbackInfoTypes.Default); + + Assert.NotNull(feedbackLoopInfo); + Assert.IsType(feedbackLoopInfo); + Assert.Equal(FeedbackInfoTypes.Default, feedbackLoopInfo.Type); + } + + [Fact] + public void FeedbackInfoInitsCustom() + { + var feedbackLoopInfo = new FeedbackInfo(FeedbackInfoTypes.Custom); + + Assert.NotNull(feedbackLoopInfo); + Assert.IsType(feedbackLoopInfo); + Assert.Equal(FeedbackInfoTypes.Custom, feedbackLoopInfo.Type); + } + } +} diff --git a/tests/Microsoft.Bot.Schema.Tests/Teams/TeamsChannelDataTests.cs b/tests/Microsoft.Bot.Schema.Tests/Teams/TeamsChannelDataTests.cs index 9f49c4a200..e2b6e4a5ef 100644 --- a/tests/Microsoft.Bot.Schema.Tests/Teams/TeamsChannelDataTests.cs +++ b/tests/Microsoft.Bot.Schema.Tests/Teams/TeamsChannelDataTests.cs @@ -31,10 +31,12 @@ public void TeamsChannelDataInits() Mri = Guid.NewGuid().ToString() } }; + var feedback = new FeedbackInfo(FeedbackInfoTypes.Default); var channelData = new TeamsChannelData(channel, eventType, team, notification, tenant, onBehalfOf) { Meeting = meeting, - Settings = settings + Settings = settings, + FeedbackLoop = feedback }; Assert.NotNull(channelData); @@ -47,6 +49,7 @@ public void TeamsChannelDataInits() Assert.Equal(settings, channelData.Settings); Assert.Equal(channel, channelData.Settings.SelectedChannel); Assert.Equal(onBehalfOf, channelData.OnBehalfOf); + Assert.Equal(feedback, channelData.FeedbackLoop); } [Fact]