Skip to content

Commit

Permalink
[C#] feat: add file and o365 card handlers (#785)
Browse files Browse the repository at this point in the history
## Linked issues

closes: #606

## Details

Add handlers for:
- `fileConsent/invoke`
- `actionableMessage/executeAction`

#### Change details

Added 3 methods to `Application`:
- `OnFileConsentAccept`
- `OnFileConsentDecline`
- `OnO365ConnectorCardAction`

Added unit tests

## Attestation Checklist

- [X] My code follows the style guidelines of this project

- I have checked for/fixed spelling, linting, and other errors
- I have commented my code for clarity
- I have made corresponding changes to the documentation (we use
[TypeDoc](https://typedoc.org/) to document our code)
- My changes generate no new warnings
- I have added tests that validates my changes, and provides sufficient
test coverage. I have tested with:
  - Local testing
  - E2E testing in Teams
- New and existing unit tests pass locally with my changes

### Additional information

> Feel free to add other relevant information below
  • Loading branch information
swatDong authored Nov 7, 2023
1 parent 3c26fc1 commit d5a35ea
Showing 4 changed files with 305 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -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<TestTurnState, TestTurnStateManager>(new()
{
RemoveRecipientMention = false,
StartTypingTimer = false
});
var ids = new List<string>();
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<TestTurnState, TestTurnStateManager>(new()
{
RemoveRecipientMention = false,
StartTypingTimer = false
});
var ids = new List<string>();
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<TestTurnState, TestTurnStateManager>(new()
{
RemoveRecipientMention = false,
StartTypingTimer = false
});
var ids = new List<string>();
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);
}
}
}
Original file line number Diff line number Diff line change
@@ -590,6 +590,81 @@ public Application<TState, TTurnStateManager> OnConfigSubmit(ConfigHandler<TStat
return this;
}

/// <summary>
/// Handles when a file consent card is accepted by the user.
/// </summary>
/// <param name="handler">Function to call when the route is triggered.</param>
/// <returns>The application instance for chaining purposes.</returns>
public Application<TState, TTurnStateManager> OnFileConsentAccept(FileConsentHandler<TState> handler)
=> OnFileConsent(handler, "accept");

/// <summary>
/// Handles when a file consent card is declined by the user.
/// </summary>
/// <param name="handler">Function to call when the route is triggered.</param>
/// <returns>The application instance for chaining purposes.</returns>
public Application<TState, TTurnStateManager> OnFileConsentDecline(FileConsentHandler<TState> handler)
=> OnFileConsent(handler, "decline");

private Application<TState, TTurnStateManager> OnFileConsent(FileConsentHandler<TState> 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<FileConsentCardResponse>(context.Activity!)) != null
&& string.Equals(fileConsentCardResponse.Action, fileConsentAction)
);
};
RouteHandler<TState> routeHandler = async (turnContext, turnState, cancellationToken) =>
{
FileConsentCardResponse fileConsentCardResponse = ActivityUtilities.GetTypedValue<FileConsentCardResponse>(turnContext.Activity) ?? new();
await handler(turnContext, turnState, fileConsentCardResponse, cancellationToken);

// Check to see if an invoke response has already been added
if (turnContext.TurnState.Get<object>(BotAdapter.InvokeResponseKey) == null)
{
Activity activity = ActivityUtilities.CreateInvokeResponseActivity();
await turnContext.SendActivityAsync(activity, cancellationToken);
}
};
AddRoute(routeSelector, routeHandler, isInvokeRoute: true);
return this;
}

/// <summary>
/// Handles O365 Connector Card Action activities.
/// </summary>
/// <param name="handler">Function to call when the route is triggered.</param>
/// <returns>The application instance for chaining purposes.</returns>
public Application<TState, TTurnStateManager> OnO365ConnectorCardAction(O365ConnectorCardActionHandler<TState> 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<TState> routeHandler = async (turnContext, turnState, cancellationToken) =>
{
O365ConnectorCardActionQuery query = ActivityUtilities.GetTypedValue<O365ConnectorCardActionQuery>(turnContext.Activity) ?? new();
await handler(turnContext, turnState, query, cancellationToken);

// Check to see if an invoke response has already been added
if (turnContext.TurnState.Get<object>(BotAdapter.InvokeResponseKey) == null)
{
Activity activity = ActivityUtilities.CreateInvokeResponseActivity();
await turnContext.SendActivityAsync(activity, cancellationToken);
}
};
AddRoute(routeSelector, routeHandler, isInvokeRoute: true);
return this;
}

/// <summary>
/// Add a handler that will execute before the turn's activity handler logic is processed.
/// <br/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema.Teams;
using Microsoft.TeamsAI.State;

namespace Microsoft.TeamsAI.Application
{
/// <summary>
/// Function for handling file consent card activities.
/// </summary>
/// <typeparam name="TState">Type of the turn state. This allows for strongly typed access to the turn state.</typeparam>
/// <param name="turnContext">A strongly-typed context object for this turn.</param>
/// <param name="turnState">The turn state object that stores arbitrary data for this turn.</param>
/// <param name="fileConsentCardResponse">The response representing the value of the invoke activity sent when the user acts on a file consent card.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
public delegate Task FileConsentHandler<TState>(ITurnContext turnContext, TState turnState, FileConsentCardResponse fileConsentCardResponse, CancellationToken cancellationToken) where TState : ITurnState<StateBase, StateBase, TempState>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Microsoft.Bot.Builder;
using Microsoft.Bot.Schema.Teams;
using Microsoft.TeamsAI.State;

namespace Microsoft.TeamsAI.Application
{
/// <summary>
/// Function for handling O365 Connector Card Action activities.
/// </summary>
/// <typeparam name="TState">Type of the turn state. This allows for strongly typed access to the turn state.</typeparam>
/// <param name="turnContext">A strongly-typed context object for this turn.</param>
/// <param name="turnState">The turn state object that stores arbitrary data for this turn.</param>
/// <param name="query">The O365 connector card HttpPOST invoke query.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects
/// or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
public delegate Task O365ConnectorCardActionHandler<TState>(ITurnContext turnContext, TState turnState, O365ConnectorCardActionQuery query, CancellationToken cancellationToken) where TState : ITurnState<StateBase, StateBase, TempState>;
}

0 comments on commit d5a35ea

Please sign in to comment.