Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

Choose a reason for hiding this comment

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

Does this mean that this feature won't work in a group chat meeting?

/// </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