Skip to content

Commit

Permalink
Support bot sdk dot net for targeted meeting notification (#6570)
Browse files Browse the repository at this point in the history
* draft PR check remote build

* Support bot sdk dot net for targeted meeting notification

* resolving comments

* resolve John's comments

Co-authored-by: Ying Du <yingdu@microsoft.com>
  • Loading branch information
yingduyingdu and Ying Du authored Dec 21, 2022
1 parent a908ca3 commit 74a8612
Show file tree
Hide file tree
Showing 12 changed files with 802 additions and 4 deletions.
25 changes: 23 additions & 2 deletions libraries/Microsoft.Bot.Builder/Teams/TeamsInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
namespace Microsoft.Bot.Builder.Teams
{
/// <summary>
/// The TeamsInfo
/// The TeamsInfo Test If Build Remote Successful
/// provides utility methods for the events and interactions that occur within Microsoft Teams.
/// </summary>
public static class TeamsInfo
Expand All @@ -29,7 +29,7 @@ public static class TeamsInfo
/// <param name="participantId">The id of the Teams meeting participant. From.AadObjectId will be used if none provided.</param>
/// <param name="tenantId">The id of the Teams meeting Tenant. TeamsChannelData.Tenant.Id will be used if none provided.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <remarks>InvalidOperationException will be thrown if meetingId, participantId or tenantId have not been
/// <remarks> <see cref="InvalidOperationException"/> will be thrown if meetingId, participantId or tenantId have not been
/// provided, and also cannot be retrieved from turnContext.Activity.</remarks>
/// <returns>Team participant channel account.</returns>
public static async Task<TeamsMeetingParticipant> GetMeetingParticipantAsync(ITurnContext turnContext, string meetingId = null, string participantId = null, string tenantId = null, CancellationToken cancellationToken = default)
Expand Down Expand Up @@ -317,6 +317,27 @@ await turnContext.Adapter.CreateConversationAsync(
return new Tuple<ConversationReference, string>(conversationReference, newActivityId);
}

/// <summary>
/// Sends a notification to meeting participants. This functionality is available only in teams meeting scoped conversations.
/// </summary>
/// <param name="turnContext">Turn context.</param>
/// <param name="notification">The notification to send to Teams.</param>
/// <param name="meetingId">The id of the Teams meeting. TeamsChannelData.Meeting.Id will be used if none provided.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <remarks>InvalidOperationException will be thrown if meetingId or notification have not been
/// provided, and also cannot be retrieved from turnContext.Activity.</remarks>
/// <returns>List of <see cref="TeamsMeetingNotificationRecipientFailureInfo"/> for whom the notification failed.</returns>
public static async Task<TeamsMeetingNotificationRecipientFailureInfos> SendMeetingNotificationAsync(ITurnContext turnContext, TeamsMeetingNotification notification, string meetingId = null, CancellationToken cancellationToken = default)
{
meetingId ??= turnContext.Activity.TeamsGetMeetingInfo()?.Id ?? throw new InvalidOperationException("This method is only valid within the scope of a MS Teams Meeting.");
notification = notification ?? throw new InvalidOperationException($"{nameof(notification)} is required.");

using (var teamsClient = GetTeamsConnectorClient(turnContext))
{
return await teamsClient.Teams.SendMeetingNotificationAsync(meetingId, notification, cancellationToken).ConfigureAwait(false);
}
}

private static async Task<IEnumerable<TeamsChannelAccount>> GetMembersAsync(IConnectorClient connectorClient, string conversationId, CancellationToken cancellationToken)
{
if (conversationId == null)
Expand Down
193 changes: 193 additions & 0 deletions libraries/Microsoft.Bot.Connector/Teams/TeamsOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,199 @@ public TeamsOperations(TeamsConnectorClient client)
return await GetResponseAsync<TeamsMeetingParticipant>(url, shouldTrace, invocationId, cancellationToken: cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Send a teams meeting notification.
/// </summary>
/// <remarks>
/// Send a notification to teams meeting particpants.
/// </remarks>
/// <param name='meetingId'>
/// Teams meeting id.
/// </param>
/// <param name='notification'>
/// Teams notification object.
/// </param>
/// <param name='customHeaders'>
/// Headers that will be added to request.
/// </param>
/// <param name='cancellationToken'>
/// The cancellation token.
/// </param>
/// <exception cref="HttpOperationException">
/// Thrown when the operation returned an invalid status code.
/// </exception>
/// <exception cref="SerializationException">
/// Thrown when unable to deserialize the response.
/// </exception>
/// <exception cref="ValidationException">
/// Thrown when an input value does not match the expected data type, range or pattern.
/// </exception>
/// <exception cref="System.ArgumentNullException">
/// Thrown when a required parameter is null.
/// </exception>
/// <returns>
/// A response object containing the response body and response headers.
/// </returns>
public async Task<HttpOperationResponse<TeamsMeetingNotificationRecipientFailureInfos>> SendMeetingNotificationMessageAsync(string meetingId, TeamsMeetingNotification notification, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
{
if (meetingId == null)
{
throw new ValidationException(ValidationRules.CannotBeNull, nameof(meetingId));
}

// Tracing
bool shouldTrace = ServiceClientTracing.IsEnabled;
string invocationId = null;
if (shouldTrace)
{
invocationId = ServiceClientTracing.NextInvocationId.ToString(CultureInfo.InvariantCulture);
Dictionary<string, object> tracingParameters = new Dictionary<string, object>();
tracingParameters.Add("meetingId", meetingId);
tracingParameters.Add("cancellationToken", cancellationToken);
ServiceClientTracing.Enter(invocationId, this, "SendMeetingNotification", tracingParameters);
}

// Construct URL
var baseUrl = Client.BaseUri.AbsoluteUri;
var url = new System.Uri(new System.Uri(baseUrl + (baseUrl.EndsWith("/", System.StringComparison.InvariantCulture) ? string.Empty : "/")), "v1/meetings/{meetingId}/notification").ToString();
url = url.Replace("{meetingId}", System.Uri.EscapeDataString(meetingId));
using var httpRequest = new HttpRequestMessage();
httpRequest.Method = new HttpMethod("POST");
httpRequest.RequestUri = new System.Uri(url);

HttpResponseMessage httpResponse = null;

// Create HTTP transport objects
#pragma warning disable CA2000 // Dispose objects before losing scope
var result = new HttpOperationResponse<TeamsMeetingNotificationRecipientFailureInfos>();
#pragma warning restore CA2000 // Dispose objects before losing scope
try
{
// Set Headers
if (customHeaders != null)
{
foreach (var header in customHeaders)
{
if (httpRequest.Headers.Contains(header.Key))
{
httpRequest.Headers.Remove(header.Key);
}

httpRequest.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
}

// Serialize Request
string requestContent = null;
if (notification != null)
{
requestContent = Rest.Serialization.SafeJsonConvert.SerializeObject(notification, Client.SerializationSettings);
httpRequest.Content = new StringContent(requestContent, System.Text.Encoding.UTF8);
httpRequest.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse("application/json; charset=utf-8");
}

// Set Credentials
if (Client.Credentials != null)
{
cancellationToken.ThrowIfCancellationRequested();
await Client.Credentials.ProcessHttpRequestAsync(httpRequest, cancellationToken).ConfigureAwait(false);
}

// Send Request
if (shouldTrace)
{
ServiceClientTracing.SendRequest(invocationId, httpRequest);
}

cancellationToken.ThrowIfCancellationRequested();
httpResponse = await Client.HttpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
if (shouldTrace)
{
ServiceClientTracing.ReceiveResponse(invocationId, httpResponse);
}

HttpStatusCode statusCode = httpResponse.StatusCode;
cancellationToken.ThrowIfCancellationRequested();
string responseContent = null;

// Create Result
result.Request = httpRequest;
result.Response = httpResponse;

if ((int)statusCode == 207)
{
// 207: if the notifications are sent only to parital number of recipients because
// the validation on some recipients’ ids failed or some recipients were not found in the roster.
// In this case, SMBA will return the user MRIs of those failed recipients in a format that was given to a bot
// (ex: if a bot sent encrypted user MRIs, return encrypted one).

responseContent = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
try
{
result.Body = Rest.Serialization.SafeJsonConvert.DeserializeObject<TeamsMeetingNotificationRecipientFailureInfos>(responseContent, Client.DeserializationSettings);
}
catch (JsonException ex)
{
if (shouldTrace)
{
ServiceClientTracing.Error(invocationId, ex);
}

throw new SerializationException("Unable to deserialize the response.", responseContent, ex);
}
}
else if ((int)statusCode != 202)
{
// 400: when Meeting Notification request payload validation fails. For instance,
// • Recipients: # of recipients is greater than what the API allows || all of recipients’ user ids were invalid
// • Surface:
// o Surface list is empty or null
// o Surface type is invalid
// o Duplicative surface type exists in one payload
// 401: if the bot token is invalid
// 403: if the bot is not allowed to send the notification.
// In this case, the payload should contain more detail error message.
// There can be many reasons: bot disabled by tenant admin, blocked during live site mitigation,
// the bot does not have a correct RSC permission for a specific surface type, etc
// 404: if a meeting chat is not found || None of the receipients were found in the roster.

// invalid/unexpected status code
var ex = new HttpOperationException($"Operation returned an invalid status code '{statusCode}'");
if (httpResponse.Content != null)
{
responseContent = await httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false);
}
else
{
responseContent = string.Empty;
}

ex.Request = new HttpRequestMessageWrapper(httpRequest, requestContent);
ex.Response = new HttpResponseMessageWrapper(httpResponse, responseContent);
if (shouldTrace)
{
ServiceClientTracing.Error(invocationId, ex);
}

throw ex;
}
}
finally
{
if (httpResponse != null)
{
httpResponse.Dispose();
}
}

if (shouldTrace)
{
ServiceClientTracing.Exit(invocationId, result);
}

return result;
}

private async Task<HttpOperationResponse<T>> GetResponseAsync<T>(string url, bool shouldTrace, string invocationId, Dictionary<string, List<string>> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
{
// Create HTTP transport objects
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,5 +112,36 @@ public static partial class TeamsOperationsExtensions
throw new InvalidOperationException("TeamsOperations with GetParticipantWithHttpMessagesAsync is required for FetchParticipantAsync.");
}
}

/// <summary>
/// Sends a notification to participants of a Teams meeting.
/// </summary>
/// <param name='operations'>
/// The operations group for this extension method.
/// </param>
/// <param name='meetingId'>
/// Team meeting Id.
/// </param>
/// <param name='notification'>
/// Team meeting notification.
/// </param>
/// <param name='cancellationToken'>
/// The cancellation token.
/// </param>
/// <returns>Information regarding which participant notifications failed.</returns>
public static async Task<TeamsMeetingNotificationRecipientFailureInfos> SendMeetingNotificationAsync(this ITeamsOperations operations, string meetingId, TeamsMeetingNotification notification, CancellationToken cancellationToken = default(CancellationToken))
{
if (operations is TeamsOperations teamsOperations)
{
using (var result = await teamsOperations.SendMeetingNotificationMessageAsync(meetingId, notification, null, cancellationToken).ConfigureAwait(false))
{
return result.Body;
}
}
else
{
throw new InvalidOperationException("TeamsOperations with SendMeetingNotificationWithHttpMessagesAsync is required for SendMeetingNotificationAsync.");
}
}
}
}
55 changes: 55 additions & 0 deletions libraries/Microsoft.Bot.Schema/Teams/OnBehalfOf.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.Bot.Schema.Teams
{
using Newtonsoft.Json;

/// <summary>
/// Specifies attribution for notifications.
/// </summary>
public partial class OnBehalfOf
{
/// <summary>
/// Initializes a new instance of the <see cref="OnBehalfOf"/> class.
/// </summary>
public OnBehalfOf()
{
CustomInit();
}

/// <summary>
/// Gets or sets the identification of the item. Default is 0.
/// </summary>
/// <value>The item id.</value>
[JsonProperty(PropertyName = "itemId")]
public int ItemId { get; set; } = 0;

/// <summary>
/// Gets or sets the mention type. Default is "person".
/// </summary>
/// <value>The mention type.</value>
[JsonProperty(PropertyName = "mentionType")]
public string MentionType { get; set; } = "person";

/// <summary>
/// Gets or sets message resource identifier (MRI) of the person on whose behalf the message is sent.
/// Message sender name would appear as "[user] through [bot name]".
/// </summary>
/// <value>The message resource identifier of the person.</value>
[JsonProperty(PropertyName = "mri")]
public string Mri { get; set; }

/// <summary>
/// Gets or sets name of the person. Used as fallback in case name resolution is unavailable.
/// </summary>
/// <value>The name of the person.</value>
[JsonProperty(PropertyName = "displayName")]
public string DisplayName { get; set; }

/// <summary>
/// An initialization method that performs custom operations like setting defaults.
/// </summary>
partial void CustomInit();
}
}
53 changes: 53 additions & 0 deletions libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotification.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

namespace Microsoft.Bot.Schema.Teams
{
using Newtonsoft.Json;

/// <summary>
/// Specifies meeting notification including channel data, type and value.
/// </summary>
public partial class TeamsMeetingNotification
{
/// <summary>
/// Initializes a new instance of the <see cref="TeamsMeetingNotification"/> class.
/// </summary>
public TeamsMeetingNotification()
{
CustomInit();
}

/// <summary>
/// Gets or sets Activty type.
/// </summary>
/// <value>
/// Activity type.
/// </value>
[JsonProperty(PropertyName = "type")]
public string Type { get; set; } = "targetedMeetingNotification";

/// <summary>
/// Gets or sets Teams meeting notification information.
/// </summary>
/// <value>
/// Teams meeting notification information.
/// </value>
[JsonProperty(PropertyName = "value")]
public TeamsMeetingNotificationInfo Value { get; set; }

/// <summary>
/// Gets or sets Teams meeting notification channel data.
/// </summary>
/// <value>
/// Teams meeting notification channel data.
/// </value>
[JsonProperty(PropertyName = "channelData")]
public TeamsMeetingNotificationChannelData ChannelData { get; set; }

/// <summary>
/// An initialization method that performs custom operations like setting defaults.
/// </summary>
partial void CustomInit();
}
}
Loading

0 comments on commit 74a8612

Please sign in to comment.