From c4232ce4e3ae95dc8f6cd977dabe0afc8deb403e Mon Sep 17 00:00:00 2001 From: CeciliaAvila Date: Wed, 10 Aug 2022 16:04:00 -0300 Subject: [PATCH 1/2] Implement GetMemberAsync for skills --- .../ChannelServiceHandlerBase.cs | 33 +++++++++++++++++++ .../Skills/CloudSkillHandler.cs | 6 ++++ .../Skills/SkillHandlerImpl.cs | 18 ++++++++++ .../ChannelServiceController.cs | 13 ++++++++ 4 files changed, 70 insertions(+) diff --git a/libraries/Microsoft.Bot.Builder/ChannelServiceHandlerBase.cs b/libraries/Microsoft.Bot.Builder/ChannelServiceHandlerBase.cs index 845b269739..53ef6a2448 100644 --- a/libraries/Microsoft.Bot.Builder/ChannelServiceHandlerBase.cs +++ b/libraries/Microsoft.Bot.Builder/ChannelServiceHandlerBase.cs @@ -129,6 +129,20 @@ public async Task> HandleGetConversationMembersAsync(strin return await OnGetConversationMembersAsync(claimsIdentity, conversationId, cancellationToken).ConfigureAwait(false); } + /// + /// Gets the account of a single conversation member. + /// + /// The authentication header. + /// The user id. + /// The conversation Id. + /// A cancellation token that can be used by other objects or threads to receive notice of cancellation. + /// A representing the result of the asynchronous operation. + public async Task HandleGetConversationMemberAsync(string authHeader, string userId, string conversationId, CancellationToken cancellationToken = default) + { + var claimsIdentity = await AuthenticateAsync(authHeader, cancellationToken).ConfigureAwait(false); + return await OnGetConversationMemberAsync(claimsIdentity, userId, conversationId, cancellationToken).ConfigureAwait(false); + } + /// /// Enumerates the members of a conversation one page at a time. /// @@ -392,6 +406,25 @@ protected virtual Task> OnGetConversationMembersAsync(Clai throw new NotImplementedException(); } + /// + /// GetConversationMember() API for Skill. + /// + /// + /// Override this method to get the account of a single conversation member. + /// + /// This REST API takes a ConversationId and UserId and returns the ChannelAccount + /// objects representing the member of the conversation. + /// + /// claimsIdentity for the bot, should have AudienceClaim, AppIdClaim and ServiceUrlClaim. + /// User ID. + /// Conversation ID. + /// The cancellation token. + /// task for a response. + protected virtual Task OnGetConversationMemberAsync(ClaimsIdentity claimsIdentity, string userId, string conversationId, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + /// /// GetConversationPagedMembers() API for Skill. /// diff --git a/libraries/Microsoft.Bot.Builder/Skills/CloudSkillHandler.cs b/libraries/Microsoft.Bot.Builder/Skills/CloudSkillHandler.cs index f905d7b07b..2d7aec0a65 100644 --- a/libraries/Microsoft.Bot.Builder/Skills/CloudSkillHandler.cs +++ b/libraries/Microsoft.Bot.Builder/Skills/CloudSkillHandler.cs @@ -132,5 +132,11 @@ protected override async Task OnUpdateActivityAsync(ClaimsIden { return await _inner.OnUpdateActivityAsync(claimsIdentity, conversationId, activityId, activity, cancellationToken).ConfigureAwait(false); } + + /// + protected override async Task OnGetConversationMemberAsync(ClaimsIdentity claimsIdentity, string userId, string conversationId, CancellationToken cancellationToken = default) + { + return await _inner.OnGetMemberAsync(claimsIdentity, userId, conversationId, cancellationToken).ConfigureAwait(false); + } } } diff --git a/libraries/Microsoft.Bot.Builder/Skills/SkillHandlerImpl.cs b/libraries/Microsoft.Bot.Builder/Skills/SkillHandlerImpl.cs index 8d5b059465..7948d010d2 100644 --- a/libraries/Microsoft.Bot.Builder/Skills/SkillHandlerImpl.cs +++ b/libraries/Microsoft.Bot.Builder/Skills/SkillHandlerImpl.cs @@ -7,6 +7,7 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; +using Microsoft.Bot.Connector; using Microsoft.Bot.Connector.Authentication; using Microsoft.Bot.Schema; using Microsoft.Extensions.Logging; @@ -91,6 +92,23 @@ internal async Task OnUpdateActivityAsync(ClaimsIdentity claim return resourceResponse ?? new ResourceResponse(Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture)); } + internal async Task OnGetMemberAsync(ClaimsIdentity claimsIdentity, string userId, string conversationId, CancellationToken cancellationToken = default) + { + var skillConversationReference = await GetSkillConversationReferenceAsync(conversationId, cancellationToken).ConfigureAwait(false); + ChannelAccount member = null; + + var callback = new BotCallbackHandler(async (turnContext, ct) => + { + var client = turnContext.TurnState.Get(); + var conversationId = turnContext.Activity.Conversation.Id; + member = await ((Conversations)client.Conversations).GetConversationMemberAsync(userId, conversationId, cancellationToken).ConfigureAwait(false); + }); + + await _adapter.ContinueConversationAsync(claimsIdentity, skillConversationReference.ConversationReference, skillConversationReference.OAuthScope, callback, cancellationToken).ConfigureAwait(false); + + return member; + } + private static void ApplySkillActivityToTurnContext(ITurnContext turnContext, Activity activity) { // adapter.ContinueConversation() sends an event activity with ContinueConversation in the name. diff --git a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ChannelServiceController.cs b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ChannelServiceController.cs index 9a43ad1070..9ed619984d 100644 --- a/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ChannelServiceController.cs +++ b/libraries/integration/Microsoft.Bot.Builder.Integration.AspNet.Core/ChannelServiceController.cs @@ -130,6 +130,19 @@ public virtual async Task GetConversationMembersAsync(string conv return new JsonResult(result, HttpHelper.BotMessageSerializerSettings); } + /// + /// GetConversationMember. + /// + /// User ID. + /// Conversation ID. + /// The ChannelAccount of the conversation member. + [HttpGet("v3/conversations/{conversationId}/members/{userId}")] + public virtual async Task GetConversationMemberAsync(string userId, string conversationId) + { + var result = await _handler.HandleGetConversationMemberAsync(HttpContext.Request.Headers["Authorization"], userId, conversationId).ConfigureAwait(false); + return new JsonResult(result, HttpHelper.BotMessageSerializerSettings); + } + /// /// GetConversationPagedMembers. /// From 9680f48fc5b94e55e413a7e5ea7b0acaf1d169ab Mon Sep 17 00:00:00 2001 From: CeciliaAvila Date: Fri, 12 Aug 2022 09:20:55 -0300 Subject: [PATCH 2/2] Add unit test --- .../Skills/CloudSkillHandlerTests.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/Microsoft.Bot.Builder.Tests/Skills/CloudSkillHandlerTests.cs b/tests/Microsoft.Bot.Builder.Tests/Skills/CloudSkillHandlerTests.cs index 7787b450c7..348188a6b9 100644 --- a/tests/Microsoft.Bot.Builder.Tests/Skills/CloudSkillHandlerTests.cs +++ b/tests/Microsoft.Bot.Builder.Tests/Skills/CloudSkillHandlerTests.cs @@ -3,10 +3,12 @@ using System; using System.Collections.Concurrent; +using System.Net.Http; using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.Builder.Skills; +using Microsoft.Bot.Connector; using Microsoft.Bot.Connector.Authentication; using Microsoft.Bot.Schema; using Moq; @@ -19,6 +21,11 @@ public class CloudSkillHandlerTests { private static readonly string TestSkillId = Guid.NewGuid().ToString("N"); private static readonly string TestAuthHeader = string.Empty; // Empty since claims extraction is being mocked + private static readonly ChannelAccount TestMember = new ChannelAccount() + { + Id = "userId", + Name = "userName" + }; [Theory] [InlineData(ActivityTypes.Message, null)] @@ -155,6 +162,23 @@ public async void TestUpdateActivityAsync() Assert.Equal(activity.Text, mockObjects.UpdateActivity.Text); } + [Fact] + public async void TestGetConversationMemberAsync() + { + // Arrange + var mockObjects = new CloudSkillHandlerTestMocks(); + var activity = new Activity(ActivityTypes.Message) { Text = $"Get Member." }; + var conversationId = await mockObjects.CreateAndApplyConversationIdAsync(activity); + + // Act + var sut = new CloudSkillHandler(mockObjects.Adapter.Object, mockObjects.Bot.Object, mockObjects.ConversationIdFactory, mockObjects.Auth.Object); + var member = await sut.HandleGetConversationMemberAsync(TestAuthHeader, TestMember.Id, conversationId); + + // Assert + Assert.Equal(TestMember.Id, member.Id); + Assert.Equal(TestMember.Name, member.Name); + } + /// /// Helper class with mocks for adapter, bot and auth needed to instantiate CloudSkillHandler and run tests. /// This class also captures the turnContext and activities sent back to the bot and the channel so we can run asserts on them. @@ -172,6 +196,7 @@ public CloudSkillHandlerTestMocks() Auth = CreateMockBotFrameworkAuthentication(); Bot = CreateMockBot(); ConversationIdFactory = new TestSkillConversationIdFactory(); + Client = CreateMockConnectorClient(); } public SkillConversationIdFactoryBase ConversationIdFactory { get; } @@ -182,6 +207,8 @@ public CloudSkillHandlerTestMocks() public Mock Bot { get; } + public IConnectorClient Client { get; } + // Gets the TurnContext created to call the bot. public TurnContext TurnContext { get; private set; } @@ -242,6 +269,8 @@ private Mock CreateMockAdapter() { // Create and capture the TurnContext so we can run assertions on it. TurnContext = new TurnContext(adapter.Object, conv.GetContinuationActivity()); + TurnContext.TurnState.Add(Client); + await botCallbackHandler(TurnContext, cancel); }); @@ -307,6 +336,21 @@ private Mock CreateMockBotFrameworkAuthentication() }); return auth; } + + private IConnectorClient CreateMockConnectorClient() + { + var httpResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(JsonConvert.SerializeObject(TestMember)) + }; + + var httpClient = new Mock(); + httpClient.Setup(x => x.SendAsync(It.IsAny(), It.IsAny())).ReturnsAsync(httpResponse); + + var client = new ConnectorClient(MicrosoftAppCredentials.Empty, httpClient.Object, false); + + return client; + } } private class TestSkillConversationIdFactory : SkillConversationIdFactoryBase