diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/ApplicationRouteTests.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/ApplicationRouteTests.cs index d0a35984d..5591b7009 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/ApplicationRouteTests.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/ApplicationRouteTests.cs @@ -1670,5 +1670,199 @@ void CaptureSend(Activity[] arg) Assert.Equal("invokeResponse", activitiesToSend[0].Type); Assert.Equivalent(expectedInvokeResponse, activitiesToSend[0].Value); } + + [Fact] + public async Task Test_OnFileConsentAccept() + { + // Arrange + Activity[]? activitiesToSend = null; + void CaptureSend(Activity[] arg) + { + activitiesToSend = arg; + } + var adapter = new SimpleAdapter(CaptureSend); + var activity1 = new Activity + { + Type = ActivityTypes.Invoke, + Name = "fileConsent/invoke", + Value = JObject.FromObject(new + { + action = "accept" + }), + Id = "test" + }; + var activity2 = new Activity + { + Type = ActivityTypes.Invoke, + Name = "fileConsent/invoke", + Value = JObject.FromObject(new + { + action = "decline" + }), + }; + var activity3 = new Activity + { + Type = ActivityTypes.Invoke, + Name = "composeExtension/queryLink" + }; + var turnContext1 = new TurnContext(adapter, activity1); + var turnContext2 = new TurnContext(adapter, activity2); + var turnContext3 = new TurnContext(adapter, activity3); + var expectedInvokeResponse = new InvokeResponse + { + Status = 200 + }; + var app = new Application(new() + { + RemoveRecipientMention = false, + StartTypingTimer = false + }); + var ids = new List(); + app.OnFileConsentAccept((turnContext, _, _, _) => + { + ids.Add(turnContext.Activity.Id); + return Task.CompletedTask; + }); + + // Act + await app.OnTurnAsync(turnContext1); + await app.OnTurnAsync(turnContext2); + await app.OnTurnAsync(turnContext3); + + // Assert + Assert.Single(ids); + Assert.Equal("test", ids[0]); + Assert.NotNull(activitiesToSend); + Assert.Equal(1, activitiesToSend.Length); + Assert.Equal("invokeResponse", activitiesToSend[0].Type); + Assert.Equivalent(expectedInvokeResponse, activitiesToSend[0].Value); + } + + [Fact] + public async Task Test_OnFileConsentDecline() + { + // Arrange + Activity[]? activitiesToSend = null; + void CaptureSend(Activity[] arg) + { + activitiesToSend = arg; + } + var adapter = new SimpleAdapter(CaptureSend); + var activity1 = new Activity + { + Type = ActivityTypes.Invoke, + Name = "fileConsent/invoke", + Value = JObject.FromObject(new + { + action = "decline" + }), + Id = "test" + }; + var activity2 = new Activity + { + Type = ActivityTypes.Invoke, + Name = "fileConsent/invoke", + Value = JObject.FromObject(new + { + action = "accept" + }), + }; + var activity3 = new Activity + { + Type = ActivityTypes.Invoke, + Name = "composeExtension/queryLink" + }; + var turnContext1 = new TurnContext(adapter, activity1); + var turnContext2 = new TurnContext(adapter, activity2); + var turnContext3 = new TurnContext(adapter, activity3); + var expectedInvokeResponse = new InvokeResponse + { + Status = 200 + }; + var app = new Application(new() + { + RemoveRecipientMention = false, + StartTypingTimer = false + }); + var ids = new List(); + app.OnFileConsentDecline((turnContext, _, _, _) => + { + ids.Add(turnContext.Activity.Id); + return Task.CompletedTask; + }); + + // Act + await app.OnTurnAsync(turnContext1); + await app.OnTurnAsync(turnContext2); + await app.OnTurnAsync(turnContext3); + + // Assert + Assert.Single(ids); + Assert.Equal("test", ids[0]); + Assert.NotNull(activitiesToSend); + Assert.Equal(1, activitiesToSend.Length); + Assert.Equal("invokeResponse", activitiesToSend[0].Type); + Assert.Equivalent(expectedInvokeResponse, activitiesToSend[0].Value); + } + + [Fact] + public async Task Test_OnO365ConnectorCardAction() + { + // Arrange + Activity[]? activitiesToSend = null; + void CaptureSend(Activity[] arg) + { + activitiesToSend = arg; + } + var adapter = new SimpleAdapter(CaptureSend); + var activity1 = new Activity + { + Type = ActivityTypes.Invoke, + Name = "actionableMessage/executeAction", + Value = new { }, + Id = "test" + }; + var activity2 = new Activity + { + Type = ActivityTypes.Event, + Name = "actionableMessage/executeAction" + }; + var activity3 = new Activity + { + Type = ActivityTypes.Invoke, + Name = "composeExtension/queryLink" + }; + var turnContext1 = new TurnContext(adapter, activity1); + var turnContext2 = new TurnContext(adapter, activity2); + var turnContext3 = new TurnContext(adapter, activity3); + var expectedInvokeResponse = new InvokeResponse + { + Status = 200 + }; + var app = new Application(new() + { + RemoveRecipientMention = false, + StartTypingTimer = false + }); + var ids = new List(); + app.OnO365ConnectorCardAction((turnContext, _, _, _) => + { + ids.Add(turnContext.Activity.Id); + return Task.CompletedTask; + }); + + // Act + await app.OnTurnAsync(turnContext1); + await app.OnTurnAsync(turnContext2); + await app.OnTurnAsync(turnContext3); + + // Assert + Assert.Single(ids); + Assert.Equal("test", ids[0]); + Assert.NotNull(activitiesToSend); + Assert.Equal(1, activitiesToSend.Length); + Assert.Equal("invokeResponse", activitiesToSend[0].Type); + Assert.Equivalent(expectedInvokeResponse, activitiesToSend[0].Value); + } } } diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Application.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Application.cs index 5ca1caf2c..193df76ea 100644 --- a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Application.cs +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/Application.cs @@ -590,6 +590,81 @@ public Application OnConfigSubmit(ConfigHandler + /// Handles when a file consent card is accepted by the user. + /// + /// Function to call when the route is triggered. + /// The application instance for chaining purposes. + public Application OnFileConsentAccept(FileConsentHandler handler) + => OnFileConsent(handler, "accept"); + + /// + /// Handles when a file consent card is declined by the user. + /// + /// Function to call when the route is triggered. + /// The application instance for chaining purposes. + public Application OnFileConsentDecline(FileConsentHandler handler) + => OnFileConsent(handler, "decline"); + + private Application OnFileConsent(FileConsentHandler handler, string fileConsentAction) + { + Verify.ParamNotNull(handler); + RouteSelector routeSelector = (context, _) => + { + FileConsentCardResponse? fileConsentCardResponse; + return Task.FromResult + ( + string.Equals(context.Activity?.Type, ActivityTypes.Invoke, StringComparison.OrdinalIgnoreCase) + && string.Equals(context.Activity?.Name, "fileConsent/invoke") + && (fileConsentCardResponse = ActivityUtilities.GetTypedValue(context.Activity!)) != null + && string.Equals(fileConsentCardResponse.Action, fileConsentAction) + ); + }; + RouteHandler routeHandler = async (turnContext, turnState, cancellationToken) => + { + FileConsentCardResponse fileConsentCardResponse = ActivityUtilities.GetTypedValue(turnContext.Activity) ?? new(); + await handler(turnContext, turnState, fileConsentCardResponse, cancellationToken); + + // Check to see if an invoke response has already been added + if (turnContext.TurnState.Get(BotAdapter.InvokeResponseKey) == null) + { + Activity activity = ActivityUtilities.CreateInvokeResponseActivity(); + await turnContext.SendActivityAsync(activity, cancellationToken); + } + }; + AddRoute(routeSelector, routeHandler, isInvokeRoute: true); + return this; + } + + /// + /// Handles O365 Connector Card Action activities. + /// + /// Function to call when the route is triggered. + /// The application instance for chaining purposes. + public Application OnO365ConnectorCardAction(O365ConnectorCardActionHandler handler) + { + Verify.ParamNotNull(handler); + RouteSelector routeSelector = (context, _) => Task.FromResult + ( + string.Equals(context.Activity?.Type, ActivityTypes.Invoke, StringComparison.OrdinalIgnoreCase) + && string.Equals(context.Activity?.Name, "actionableMessage/executeAction") + ); + RouteHandler routeHandler = async (turnContext, turnState, cancellationToken) => + { + O365ConnectorCardActionQuery query = ActivityUtilities.GetTypedValue(turnContext.Activity) ?? new(); + await handler(turnContext, turnState, query, cancellationToken); + + // Check to see if an invoke response has already been added + if (turnContext.TurnState.Get(BotAdapter.InvokeResponseKey) == null) + { + Activity activity = ActivityUtilities.CreateInvokeResponseActivity(); + await turnContext.SendActivityAsync(activity, cancellationToken); + } + }; + AddRoute(routeSelector, routeHandler, isInvokeRoute: true); + return this; + } + /// /// Add a handler that will execute before the turn's activity handler logic is processed. ///
diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/FileConsentCardHandler.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/FileConsentCardHandler.cs new file mode 100644 index 000000000..c4ddd2cce --- /dev/null +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/FileConsentCardHandler.cs @@ -0,0 +1,18 @@ +using Microsoft.Bot.Builder; +using Microsoft.Bot.Schema.Teams; +using Microsoft.TeamsAI.State; + +namespace Microsoft.TeamsAI.Application +{ + /// + /// Function for handling file consent card activities. + /// + /// Type of the turn state. This allows for strongly typed access to the turn state. + /// A strongly-typed context object for this turn. + /// The turn state object that stores arbitrary data for this turn. + /// The response representing the value of the invoke activity sent when the user acts on a file consent card. + /// 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. + public delegate Task FileConsentHandler(ITurnContext turnContext, TState turnState, FileConsentCardResponse fileConsentCardResponse, CancellationToken cancellationToken) where TState : ITurnState; +} diff --git a/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/O365ConnectorCardActionHandler.cs b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/O365ConnectorCardActionHandler.cs new file mode 100644 index 000000000..d5f39d30b --- /dev/null +++ b/dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI/Application/O365ConnectorCardActionHandler.cs @@ -0,0 +1,18 @@ +using Microsoft.Bot.Builder; +using Microsoft.Bot.Schema.Teams; +using Microsoft.TeamsAI.State; + +namespace Microsoft.TeamsAI.Application +{ + /// + /// Function for handling O365 Connector Card Action activities. + /// + /// Type of the turn state. This allows for strongly typed access to the turn state. + /// A strongly-typed context object for this turn. + /// The turn state object that stores arbitrary data for this turn. + /// The O365 connector card HttpPOST invoke query. + /// 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. + public delegate Task O365ConnectorCardActionHandler(ITurnContext turnContext, TState turnState, O365ConnectorCardActionQuery query, CancellationToken cancellationToken) where TState : ITurnState; +}