Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of Teams batch APIs #6655

Merged
merged 12 commits into from
Aug 16, 2023
134 changes: 134 additions & 0 deletions libraries/Microsoft.Bot.Builder/Teams/TeamsInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,140 @@ public static async Task<MeetingNotificationResponse> SendMeetingNotificationAsy
}
}

/// <summary>
/// Sends a message to the provided list of Teams members.
/// </summary>
/// <param name="turnContext"> Turn context. </param>
/// <param name="activity"> The activity to send. </param>
/// <param name="teamsMembers"> The list of members. </param>
/// <param name="tenantId"> The tenant ID. </param>
/// <param name="cancellationToken"> The cancellation token. </param>
/// <returns> The operation Id. </returns>
public static async Task<string> SendMessageToListOfUsersAsync(ITurnContext turnContext, IActivity activity, List<object> teamsMembers, string tenantId, CancellationToken cancellationToken = default)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why we're using object and not specifying a concrete type for list of members?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We created a TeamMember model with a string Id property to handle this.

{
activity = activity ?? throw new InvalidOperationException($"{nameof(activity)} is required.");
teamsMembers = teamsMembers ?? throw new InvalidOperationException($"{nameof(teamsMembers)} is required.");
tenantId = tenantId ?? throw new InvalidOperationException($"{nameof(tenantId)} is required.");

using (var teamsClient = GetTeamsConnectorClient(turnContext))
{
return await teamsClient.Teams.SendMessageToListOfUsersAsync(activity, teamsMembers, tenantId, cancellationToken).ConfigureAwait(false);
}
}

/// <summary>
/// Sends a message to all the users in a tenant.
/// </summary>
/// <param name="turnContext"> The turn context. </param>
/// <param name="activity"> The activity to send to the tenant. </param>
/// <param name="tenantId"> The tenant ID. </param>
/// <param name="cancellationToken"> The cancellation token. </param>
/// <returns> The operation Id. </returns>
public static async Task<string> SendMessageToAllUsersInTenantAsync(ITurnContext turnContext, IActivity activity, string tenantId, CancellationToken cancellationToken = default)
{
activity = activity ?? throw new InvalidOperationException($"{nameof(activity)} is required.");
tenantId = tenantId ?? throw new InvalidOperationException($"{nameof(tenantId)} is required.");

using (var teamsClient = GetTeamsConnectorClient(turnContext))
{
return await teamsClient.Teams.SendMessageToAllUsersInTenantAsync(activity, tenantId, cancellationToken).ConfigureAwait(false);
}
}

/// <summary>
/// Sends a message to all the users in a team.
/// </summary>
/// <param name="turnContext"> The turn context. </param>
/// <param name="activity"> The activity to send to the users in the team. </param>
/// <param name="teamId"> The team ID. </param>
/// <param name="tenantId"> The tenant ID. </param>
/// <param name="cancellationToken"> The cancellation token. </param>
/// <returns>The operation Id.</returns>
public static async Task<string> SendMessageToAllUsersInTeamAsync(ITurnContext turnContext, IActivity activity, string teamId, string tenantId, CancellationToken cancellationToken = default)
{
activity = activity ?? throw new InvalidOperationException($"{nameof(activity)} is required.");
teamId = teamId ?? throw new InvalidOperationException($"{nameof(teamId)} is required.");
tenantId = tenantId ?? throw new InvalidOperationException($"{nameof(tenantId)} is required.");

using (var teamsClient = GetTeamsConnectorClient(turnContext))
{
return await teamsClient.Teams.SendMessageToAllUsersInTeamAsync(activity, teamId, tenantId, cancellationToken).ConfigureAwait(false);
}
}

/// <summary>
/// Sends a message to the provided list of Teams channels.
/// </summary>
/// <param name="turnContext"> The turn context. </param>
/// <param name="activity"> The activity to send. </param>
/// <param name="channelsMembers"> The list of channels. </param>
/// <param name="tenantId"> The tenant ID. </param>
/// <param name="cancellationToken"> The cancellation token. </param>
/// <returns> The operation Id. </returns>
public static async Task<string> SendMessageToListOfChannelsAsync(ITurnContext turnContext, IActivity activity, List<object> channelsMembers, string tenantId, CancellationToken cancellationToken = default)
{
activity = activity ?? throw new InvalidOperationException($"{nameof(activity)} is required.");
channelsMembers = channelsMembers ?? throw new InvalidOperationException($"{nameof(channelsMembers)} is required.");
tenantId = tenantId ?? throw new InvalidOperationException($"{nameof(tenantId)} is required.");

using (var teamsClient = GetTeamsConnectorClient(turnContext))
{
return await teamsClient.Teams.SendMessageToListOfChannelsAsync(activity, channelsMembers, tenantId, cancellationToken).ConfigureAwait(false);
}
}

/// <summary>
/// Gets the state of an operation.
/// </summary>
/// <param name="turnContext"> Turn context. </param>
/// <param name="operationId"> The operationId to get the state of. </param>
/// <param name="cancellationToken"> The cancellation token. </param>
/// <returns> The state and responses of the operation. </returns>
public static async Task<BatchOperationState> GetOperationStateAsync(ITurnContext turnContext, string operationId, CancellationToken cancellationToken = default)
{
operationId = operationId ?? throw new InvalidOperationException($"{nameof(operationId)} is required.");

using (var teamsClient = GetTeamsConnectorClient(turnContext))
{
return await teamsClient.Teams.GetOperationStateAsync(operationId, cancellationToken).ConfigureAwait(false);
}
}

/// <summary>
/// Gets the failed entries of a batch operation.
/// </summary>
/// <param name="turnContext"> The turn context. </param>
/// <param name="operationId"> The operationId to get the failed entries of. </param>
/// <param name="continuationToken"> The continuation token. </param>
/// <param name="cancellationToken"> The cancellation token. </param>
/// <returns> The list of failed entries of the operation. </returns>
public static async Task<BatchFailedEntriesResponse> GetPagedFailedEntriesAsync(ITurnContext turnContext, string operationId, string continuationToken = null, CancellationToken cancellationToken = default)
{
operationId = operationId ?? throw new InvalidOperationException($"{nameof(operationId)} is required.");

using (var teamsClient = GetTeamsConnectorClient(turnContext))
{
return await teamsClient.Teams.GetPagedFailedEntriesAsync(operationId, continuationToken, cancellationToken).ConfigureAwait(false);
}
}

/// <summary>
/// Cancels a batch operation by its id.
/// </summary>
/// <param name="turnContext"> The turn context. </param>
/// <param name="operationId"> The id of the operation to cancel. </param>
/// <param name="cancellationToken"> The cancellation token. </param>
/// <returns> A <see cref="Task"/> representing the asynchronous operation. </returns>
public static async Task CancelOperationAsync(ITurnContext turnContext, string operationId, CancellationToken cancellationToken = default)
{
operationId = operationId ?? throw new InvalidOperationException($"{nameof(operationId)} is required.");

using (var teamsClient = GetTeamsConnectorClient(turnContext))
{
await teamsClient.Teams.CancelOperationAsync(operationId, cancellationToken).ConfigureAwait(false);
}
}

private static async Task<IEnumerable<TeamsChannelAccount>> GetMembersAsync(IConnectorClient connectorClient, string conversationId, CancellationToken cancellationToken)
{
if (conversationId == null)
Expand Down
54 changes: 54 additions & 0 deletions libraries/Microsoft.Bot.Connector/RetryAction.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Bot.Connector.Authentication;

namespace Microsoft.Bot.Connector
{
/// <summary>
/// Retries asynchronous operations. In case of errors, it collects and returns exceptions in an AggregateException object.
/// </summary>
public static class RetryAction
{
/// <summary>
/// Starts the retry of the action requested.
/// </summary>
/// <typeparam name="TResult">The result expected from the action performed.</typeparam>
/// <param name="task">A reference to the action to retry.</param>
/// <param name="retryExceptionHandler">A reference to the method that handles exceptions.</param>
/// <returns>A result object.</returns>
public static async Task<TResult> RunAsync<TResult>(Func<Task<TResult>> task, Func<Exception, int, RetryParams> retryExceptionHandler)
{
RetryParams retry;
var exceptions = new List<Exception>();
var currentRetryCount = 0;

do
{
try
{
return await task().ConfigureAwait(false);
}
#pragma warning disable CA1031 // Do not catch general exception types (this is a generic catch all to handle retries)
catch (Exception ex)
#pragma warning restore CA1031 // Do not catch general exception types
{
exceptions.Add(ex);
retry = retryExceptionHandler(ex, currentRetryCount);
}

if (retry.ShouldRetry)
{
currentRetryCount++;
await Task.Delay(retry.RetryAfter.WithJitter()).ConfigureAwait(false);
}
}
while (retry.ShouldRetry);

throw new AggregateException("Failed to perform the required operation.", exceptions);
}
}
}
Loading
Loading