diff --git a/libraries/Microsoft.Bot.Builder/Teams/TeamsInfo.cs b/libraries/Microsoft.Bot.Builder/Teams/TeamsInfo.cs
index ed6ca2f4f1..efd83667eb 100644
--- a/libraries/Microsoft.Bot.Builder/Teams/TeamsInfo.cs
+++ b/libraries/Microsoft.Bot.Builder/Teams/TeamsInfo.cs
@@ -16,7 +16,7 @@
namespace Microsoft.Bot.Builder.Teams
{
///
- /// The TeamsInfo
+ /// The TeamsInfo Test If Build Remote Successful
/// provides utility methods for the events and interactions that occur within Microsoft Teams.
///
public static class TeamsInfo
@@ -29,7 +29,7 @@ public static class TeamsInfo
/// The id of the Teams meeting participant. From.AadObjectId will be used if none provided.
/// The id of the Teams meeting Tenant. TeamsChannelData.Tenant.Id will be used if none provided.
/// Cancellation token.
- /// InvalidOperationException will be thrown if meetingId, participantId or tenantId have not been
+ /// will be thrown if meetingId, participantId or tenantId have not been
/// provided, and also cannot be retrieved from turnContext.Activity.
/// Team participant channel account.
public static async Task GetMeetingParticipantAsync(ITurnContext turnContext, string meetingId = null, string participantId = null, string tenantId = null, CancellationToken cancellationToken = default)
@@ -317,6 +317,27 @@ await turnContext.Adapter.CreateConversationAsync(
return new Tuple(conversationReference, newActivityId);
}
+ ///
+ /// Sends a notification to meeting participants. This functionality is available only in teams meeting scoped conversations.
+ ///
+ /// Turn context.
+ /// The notification to send to Teams.
+ /// The id of the Teams meeting. TeamsChannelData.Meeting.Id will be used if none provided.
+ /// Cancellation token.
+ /// InvalidOperationException will be thrown if meetingId or notification have not been
+ /// provided, and also cannot be retrieved from turnContext.Activity.
+ /// List of for whom the notification failed.
+ public static async Task 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> GetMembersAsync(IConnectorClient connectorClient, string conversationId, CancellationToken cancellationToken)
{
if (conversationId == null)
diff --git a/libraries/Microsoft.Bot.Connector/Teams/TeamsOperations.cs b/libraries/Microsoft.Bot.Connector/Teams/TeamsOperations.cs
index 418e1898d3..9116668f59 100644
--- a/libraries/Microsoft.Bot.Connector/Teams/TeamsOperations.cs
+++ b/libraries/Microsoft.Bot.Connector/Teams/TeamsOperations.cs
@@ -287,6 +287,199 @@ public TeamsOperations(TeamsConnectorClient client)
return await GetResponseAsync(url, shouldTrace, invocationId, cancellationToken: cancellationToken).ConfigureAwait(false);
}
+ ///
+ /// Send a teams meeting notification.
+ ///
+ ///
+ /// Send a notification to teams meeting particpants.
+ ///
+ ///
+ /// Teams meeting id.
+ ///
+ ///
+ /// Teams notification object.
+ ///
+ ///
+ /// Headers that will be added to request.
+ ///
+ ///
+ /// The cancellation token.
+ ///
+ ///
+ /// Thrown when the operation returned an invalid status code.
+ ///
+ ///
+ /// Thrown when unable to deserialize the response.
+ ///
+ ///
+ /// Thrown when an input value does not match the expected data type, range or pattern.
+ ///
+ ///
+ /// Thrown when a required parameter is null.
+ ///
+ ///
+ /// A response object containing the response body and response headers.
+ ///
+ public async Task> SendMeetingNotificationMessageAsync(string meetingId, TeamsMeetingNotification notification, Dictionary> 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 tracingParameters = new Dictionary();
+ 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();
+#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(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> GetResponseAsync(string url, bool shouldTrace, string invocationId, Dictionary> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken))
{
// Create HTTP transport objects
diff --git a/libraries/Microsoft.Bot.Connector/Teams/TeamsOperationsExtensions.cs b/libraries/Microsoft.Bot.Connector/Teams/TeamsOperationsExtensions.cs
index f53048a595..631e8e21e3 100644
--- a/libraries/Microsoft.Bot.Connector/Teams/TeamsOperationsExtensions.cs
+++ b/libraries/Microsoft.Bot.Connector/Teams/TeamsOperationsExtensions.cs
@@ -112,5 +112,36 @@ public static partial class TeamsOperationsExtensions
throw new InvalidOperationException("TeamsOperations with GetParticipantWithHttpMessagesAsync is required for FetchParticipantAsync.");
}
}
+
+ ///
+ /// Sends a notification to participants of a Teams meeting.
+ ///
+ ///
+ /// The operations group for this extension method.
+ ///
+ ///
+ /// Team meeting Id.
+ ///
+ ///
+ /// Team meeting notification.
+ ///
+ ///
+ /// The cancellation token.
+ ///
+ /// Information regarding which participant notifications failed.
+ public static async Task 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.");
+ }
+ }
}
}
diff --git a/libraries/Microsoft.Bot.Schema/Teams/OnBehalfOf.cs b/libraries/Microsoft.Bot.Schema/Teams/OnBehalfOf.cs
new file mode 100644
index 0000000000..844a31d5eb
--- /dev/null
+++ b/libraries/Microsoft.Bot.Schema/Teams/OnBehalfOf.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Bot.Schema.Teams
+{
+ using Newtonsoft.Json;
+
+ ///
+ /// Specifies attribution for notifications.
+ ///
+ public partial class OnBehalfOf
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public OnBehalfOf()
+ {
+ CustomInit();
+ }
+
+ ///
+ /// Gets or sets the identification of the item. Default is 0.
+ ///
+ /// The item id.
+ [JsonProperty(PropertyName = "itemId")]
+ public int ItemId { get; set; } = 0;
+
+ ///
+ /// Gets or sets the mention type. Default is "person".
+ ///
+ /// The mention type.
+ [JsonProperty(PropertyName = "mentionType")]
+ public string MentionType { get; set; } = "person";
+
+ ///
+ /// 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]".
+ ///
+ /// The message resource identifier of the person.
+ [JsonProperty(PropertyName = "mri")]
+ public string Mri { get; set; }
+
+ ///
+ /// Gets or sets name of the person. Used as fallback in case name resolution is unavailable.
+ ///
+ /// The name of the person.
+ [JsonProperty(PropertyName = "displayName")]
+ public string DisplayName { get; set; }
+
+ ///
+ /// An initialization method that performs custom operations like setting defaults.
+ ///
+ partial void CustomInit();
+ }
+}
diff --git a/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotification.cs b/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotification.cs
new file mode 100644
index 0000000000..a5048f958b
--- /dev/null
+++ b/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotification.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Bot.Schema.Teams
+{
+ using Newtonsoft.Json;
+
+ ///
+ /// Specifies meeting notification including channel data, type and value.
+ ///
+ public partial class TeamsMeetingNotification
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TeamsMeetingNotification()
+ {
+ CustomInit();
+ }
+
+ ///
+ /// Gets or sets Activty type.
+ ///
+ ///
+ /// Activity type.
+ ///
+ [JsonProperty(PropertyName = "type")]
+ public string Type { get; set; } = "targetedMeetingNotification";
+
+ ///
+ /// Gets or sets Teams meeting notification information.
+ ///
+ ///
+ /// Teams meeting notification information.
+ ///
+ [JsonProperty(PropertyName = "value")]
+ public TeamsMeetingNotificationInfo Value { get; set; }
+
+ ///
+ /// Gets or sets Teams meeting notification channel data.
+ ///
+ ///
+ /// Teams meeting notification channel data.
+ ///
+ [JsonProperty(PropertyName = "channelData")]
+ public TeamsMeetingNotificationChannelData ChannelData { get; set; }
+
+ ///
+ /// An initialization method that performs custom operations like setting defaults.
+ ///
+ partial void CustomInit();
+ }
+}
diff --git a/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationChannelData.cs b/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationChannelData.cs
new file mode 100644
index 0000000000..3f4e488231
--- /dev/null
+++ b/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationChannelData.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Bot.Schema.Teams
+{
+ using System.Collections.Generic;
+ using Newtonsoft.Json;
+
+ ///
+ /// Container for list of information for a meeting .
+ ///
+ public partial class TeamsMeetingNotificationChannelData
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TeamsMeetingNotificationChannelData()
+ {
+ CustomInit();
+ }
+
+ ///
+ /// Gets or sets the for user attribution.
+ ///
+ /// The meeting notification's .
+#pragma warning disable CA2227 // Collection properties should be read only (we can't change this without breaking binary compat)>
+ [JsonProperty(PropertyName = "OnBehalfOf")]
+ public IList OnBehalfOf { get; set; }
+#pragma warning restore CA2227 // Collection properties should be read only
+
+ ///
+ /// An initialization method that performs custom operations like setting defaults.
+ ///
+ partial void CustomInit();
+ }
+}
diff --git a/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationInfo.cs b/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationInfo.cs
new file mode 100644
index 0000000000..9917cd94a4
--- /dev/null
+++ b/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationInfo.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace Microsoft.Bot.Schema.Teams
+{
+ ///
+ /// Specifies the container for what is required to send a meeting notification to recipients.
+ ///
+ public partial class TeamsMeetingNotificationInfo
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TeamsMeetingNotificationInfo()
+ {
+ CustomInit();
+ }
+
+ ///
+ /// Gets or sets the collection of recipients of the notification.
+ ///
+ ///
+ /// The collection of recipients of the notification.
+ ///
+ [JsonProperty(PropertyName = "recipients")]
+#pragma warning disable CA2227 // Collection properties should be read only (we can't change this without breaking binary compat)
+ public IList Recipients { get; set; }
+
+ ///
+ /// Gets or sets the collection of surfaces on which to show the notification.
+ ///
+ ///
+ /// The collection of surfaces on which to show the notification.
+ ///
+ [JsonProperty(PropertyName = "surfaces")]
+ public IList Surfaces { get; set; }
+#pragma warning restore CA2227 // Collection properties should be read only
+
+ ///
+ /// An initialization method that performs custom operations like setting defaults.
+ ///
+
+ ///
+ /// An initialization method that performs custom operations like setting defaults.
+ ///
+ partial void CustomInit();
+ }
+}
diff --git a/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationOnBehalfOf.cs b/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationOnBehalfOf.cs
new file mode 100644
index 0000000000..9166a165bd
--- /dev/null
+++ b/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationOnBehalfOf.cs
@@ -0,0 +1,55 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Bot.Schema.Teams
+{
+ using Newtonsoft.Json;
+
+ ///
+ /// Specifies attribution for notifications.
+ ///
+ public partial class TeamsMeetingNotificationOnBehalfOf
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TeamsMeetingNotificationOnBehalfOf()
+ {
+ CustomInit();
+ }
+
+ ///
+ /// Gets or sets the identification of the item. Default is 0.
+ ///
+ /// The item id.
+ [JsonProperty(PropertyName = "itemId")]
+ public int ItemId { get; set; } = 0;
+
+ ///
+ /// Gets or sets the mention type. Default is "person".
+ ///
+ /// The mention type.
+ [JsonProperty(PropertyName = "mentionType")]
+ public string MentionType { get; set; } = "person";
+
+ ///
+ /// 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]".
+ ///
+ /// The message resource identifier of the person.
+ [JsonProperty(PropertyName = "mri")]
+ public string Mri { get; set; }
+
+ ///
+ /// Gets or sets name of the person. Used as fallback in case name resolution is unavailable.
+ ///
+ /// The name of the person.
+ [JsonProperty(PropertyName = "displayName")]
+ public string DisplayName { get; set; }
+
+ ///
+ /// An initialization method that performs custom operations like setting defaults.
+ ///
+ partial void CustomInit();
+ }
+}
diff --git a/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationRecipientFailureInfo.cs b/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationRecipientFailureInfo.cs
new file mode 100644
index 0000000000..7bd3ac9ed8
--- /dev/null
+++ b/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationRecipientFailureInfo.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Newtonsoft.Json;
+
+namespace Microsoft.Bot.Schema.Teams
+{
+ ///
+ /// Information regarding failure to notify a recipient of a .
+ ///
+ public class TeamsMeetingNotificationRecipientFailureInfo
+ {
+ ///
+ /// Gets or sets the mri for a recipient failure.
+ ///
+ /// The type of this notification container.
+ [JsonProperty(PropertyName = "recipientMri")]
+ public string RecipientMri { get; set; }
+
+ ///
+ /// Gets or sets the error code for a .
+ ///
+ /// The error code for a .
+ [JsonProperty(PropertyName = "errorcode")]
+ public string ErrorCode { get; set; }
+
+ ///
+ /// Gets or sets the failure reason for a failure.
+ ///
+ /// The reason why a participant failed.
+ [JsonProperty(PropertyName = "failureReason")]
+ public string FailureReason { get; set; }
+ }
+}
diff --git a/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationRecipientFailureInfos.cs b/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationRecipientFailureInfos.cs
new file mode 100644
index 0000000000..57a39202c5
--- /dev/null
+++ b/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationRecipientFailureInfos.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Collections.Generic;
+using Newtonsoft.Json;
+
+namespace Microsoft.Bot.Schema.Teams
+{
+ ///
+ /// Container for , which is the result of a
+ /// failure to notify recipients of a .
+ ///
+ public partial class TeamsMeetingNotificationRecipientFailureInfos
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TeamsMeetingNotificationRecipientFailureInfos()
+ {
+ CustomInit();
+ }
+
+ ///
+ /// Gets or sets the list of .
+ ///
+ /// The list of recipients who did not receive a including error information.
+ [JsonProperty(PropertyName = "recipientsFailureInfo")]
+#pragma warning disable CA2227 // Collection properties should be read only (we can't change this without breaking binary compat)>
+ public IList RecipientsFailureInfo { get; set; }
+#pragma warning restore CA2227 // Collection properties should be read only
+
+ ///
+ /// An initialization method that performs custom operations like setting defaults.
+ ///
+ partial void CustomInit();
+ }
+}
diff --git a/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationSurface.cs b/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationSurface.cs
new file mode 100644
index 0000000000..f81a6c6c19
--- /dev/null
+++ b/libraries/Microsoft.Bot.Schema/Teams/TeamsMeetingNotificationSurface.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+namespace Microsoft.Bot.Schema.Teams
+{
+ using Newtonsoft.Json;
+
+ ///
+ /// Specifies if a notification is to be sent for the mentions.
+ ///
+ public partial class TeamsMeetingNotificationSurface
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TeamsMeetingNotificationSurface()
+ {
+ CustomInit();
+ }
+
+ ///
+ /// Gets or sets the value indicating where the signal will be rendered in the meeting UX.
+ /// Note: only one instance of surface type is allowed per request.
+ ///
+ ///
+ /// The value indicating where the signal will be rendered in the meeting UX.
+ ///
+ [JsonProperty(PropertyName = "surface")]
+ public string Surface { get; set; } = "meetingStage";
+
+ ///
+ /// Gets or sets the content type of this .
+ ///
+ ///
+ /// The content type of this .
+ ///
+ [JsonProperty(PropertyName = "contentType")]
+ public string ContentType { get; set; } = "task";
+
+ ///
+ /// Gets or sets the content for this .
+ ///
+ ///
+ /// The content type of this .
+ ///
+ [JsonProperty(PropertyName = "content")]
+ public TaskModuleContinueResponse Content { get; set; }
+
+ ///
+ /// An initialization method that performs custom operations like setting defaults.
+ ///
+ partial void CustomInit();
+ }
+}
diff --git a/tests/Microsoft.Bot.Builder.Tests/Teams/TeamsInfoTests.cs b/tests/Microsoft.Bot.Builder.Tests/Teams/TeamsInfoTests.cs
index 3de65e7006..6d6928d3a7 100644
--- a/tests/Microsoft.Bot.Builder.Tests/Teams/TeamsInfoTests.cs
+++ b/tests/Microsoft.Bot.Builder.Tests/Teams/TeamsInfoTests.cs
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
@@ -12,7 +13,9 @@
using Microsoft.Bot.Connector.Authentication;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Schema.Teams;
+using Microsoft.Rest;
using Moq;
+using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
@@ -350,6 +353,59 @@ public async Task TestGetMemberNoTeamAsync()
await handler.OnTurnAsync(turnContext);
}
+ [Theory]
+ [InlineData("202")]
+ [InlineData("207")]
+ [InlineData("400")]
+ [InlineData("403")]
+ public async Task TestSendMeetingNotificationAsync(string statusCode)
+ {
+ // 202: accepted
+ // 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).
+
+ // 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
+ // 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
+
+ var baseUri = new Uri("https://test.coffee");
+ var customHttpClient = new HttpClient(new RosterHttpMessageHandler());
+
+ // Set a special base address so then we can make sure the connector client is honoring this http client
+ customHttpClient.BaseAddress = baseUri;
+ var connectorClient = new ConnectorClient(new Uri("http://localhost/"), new MicrosoftAppCredentials(string.Empty, string.Empty), customHttpClient);
+
+ var activity = new Activity
+ {
+ Type = "targetedMeetingNotification",
+ Text = "Test-SendMeetingNotificationAsync",
+ ChannelId = Channels.Msteams,
+ ServiceUrl = "https://test.coffee",
+ From = new ChannelAccount()
+ {
+ Id = "id-1",
+
+ // Hack for test. use the Name field to pass expected status code to test code
+ Name = statusCode
+ },
+ Conversation = new ConversationAccount() { Id = "conversation-id" }
+ };
+
+ var turnContext = new TurnContext(new SimpleAdapter(), activity);
+ turnContext.TurnState.Add(connectorClient);
+ var handler = new TestTeamsActivityHandler();
+ await handler.OnTurnAsync(turnContext);
+ }
+
private class TestTeamsActivityHandler : TeamsActivityHandler
{
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
@@ -382,6 +438,9 @@ public override async Task OnTurnAsync(ITurnContext turnContext, CancellationTok
case "Test-GetMeetingInfoAsync":
await CallTeamsInfoGetMeetingInfoAsync(turnContext);
break;
+ case "Test-SendMeetingNotificationAsync":
+ await CallSendMeetingNotificationAsync(turnContext);
+ break;
default:
Assert.True(false);
break;
@@ -457,6 +516,91 @@ private async Task CallTeamsInfoGetMeetingInfoAsync(ITurnContext turnContext)
Assert.Equal("meetingConversationId-1", meeting.Conversation.Id);
}
+ private TeamsMeetingNotification GetTeamsMeetingNotification(ChannelAccount from)
+ {
+ var recipients = new List { from.Id };
+
+ if (from.Name == "207")
+ {
+ recipients.Add("failingid");
+ }
+
+ var surface = new TeamsMeetingNotificationSurface
+ {
+ Content = new TaskModuleContinueResponse
+ {
+ Value = new TaskModuleTaskInfo
+ {
+ Title = "title here",
+ Height = 3,
+ Width = 2,
+ }
+ }
+ };
+
+ var value = new TeamsMeetingNotificationInfo
+ {
+ Recipients = recipients,
+ Surfaces = new[] { surface }
+ };
+
+ var obo = new TeamsMeetingNotificationOnBehalfOf
+ {
+ DisplayName = from.Name,
+ Mri = from.Id
+ };
+
+ var channelData = new TeamsMeetingNotificationChannelData
+ {
+ OnBehalfOf = new[] { obo }
+ };
+
+ return new TeamsMeetingNotification
+ {
+ Value = value,
+ ChannelData = channelData
+ };
+ }
+
+ private async Task CallSendMeetingNotificationAsync(ITurnContext turnContext)
+ {
+ var from = turnContext.Activity.From;
+
+ try
+ {
+ var failedParticipants = await TeamsInfo.SendMeetingNotificationAsync(turnContext, GetTeamsMeetingNotification(from), "meeting-id").ConfigureAwait(false);
+
+ switch (from.Name)
+ {
+ case "207":
+ Assert.Equal("failingid", failedParticipants.RecipientsFailureInfo.First().RecipientMri);
+ break;
+ case "202":
+ Assert.Null(failedParticipants);
+ break;
+ default:
+ throw new InvalidOperationException($"Expected {nameof(HttpOperationException)} with response status code {from.Name}.");
+ }
+ }
+ catch (HttpOperationException ex)
+ {
+ Assert.Equal(from.Name, ((int)ex.Response.StatusCode).ToString());
+ var errorResponse = JsonConvert.DeserializeObject(ex.Response.Content);
+
+ switch (from.Name)
+ {
+ case "400":
+ Assert.Equal("BadSyntax", errorResponse.Error.Code);
+ break;
+ case "403":
+ Assert.Equal("BotNotInConversationRoster", errorResponse.Error.Code);
+ break;
+ default:
+ throw new InvalidOperationException($"Expected {nameof(HttpOperationException)} with response status code {from.Name}.");
+ }
+ }
+ }
+
private async Task CallGroupChatGetMembersAsync(ITurnContext turnContext)
{
var members = (await TeamsInfo.GetMembersAsync(turnContext)).ToArray();
@@ -490,7 +634,7 @@ private async Task CallGetChannelsAsync(ITurnContext turnContext)
private class RosterHttpMessageHandler : HttpMessageHandler
{
- protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
+ protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(HttpStatusCode.OK);
@@ -639,7 +783,41 @@ protected override Task SendAsync(HttpRequestMessage reques
response.Content = new StringContent(content.ToString());
}
- return Task.FromResult(response);
+ // Post meeting notification
+ else if (request.RequestUri.PathAndQuery.EndsWith("v1/meetings/meeting-id/notification"))
+ {
+ var responseBody = await request.Content.ReadAsStringAsync().ConfigureAwait(false);
+ var notification = JsonConvert.DeserializeObject(responseBody);
+ var obo = notification.ChannelData.OnBehalfOf.First();
+
+ // hack displayname as expected status code, for testing
+ switch (obo.DisplayName)
+ {
+ case "207":
+ var failureInfo = new TeamsMeetingNotificationRecipientFailureInfo { RecipientMri = notification.Value.Recipients.First(r => !r.Equals(obo.Mri, StringComparison.OrdinalIgnoreCase)) };
+ var infos = new TeamsMeetingNotificationRecipientFailureInfos
+ {
+ RecipientsFailureInfo = new List { failureInfo }
+ };
+
+ response.Content = new StringContent(JsonConvert.SerializeObject(infos));
+ response.StatusCode = HttpStatusCode.MultiStatus;
+ break;
+ case "403":
+ response.Content = new StringContent("{\"error\":{\"code\":\"BotNotInConversationRoster\"}}");
+ response.StatusCode = HttpStatusCode.Forbidden;
+ break;
+ case "400":
+ response.Content = new StringContent("{\"error\":{\"code\":\"BadSyntax\"}}");
+ response.StatusCode = HttpStatusCode.BadRequest;
+ break;
+ default:
+ response.StatusCode = HttpStatusCode.Accepted;
+ break;
+ }
+ }
+
+ return response;
}
}