/// <summary> /// Forwards an activity to a skill (bot). /// </summary> /// <remarks>NOTE: Forwarding an activity to a skill will flush UserState and ConversationState changes so that skill has accurate state.</remarks> /// <param name="turnContext">turnContext.</param> /// <param name="skill">A <see cref="BotFrameworkSkill"/> instance with the skill information.</param> /// <param name="skillHostEndpoint">The callback Url for the skill host.</param> /// <param name="activity">activity to forward.</param> /// <param name="cancellationToken">cancellation Token.</param> /// <returns>Async task with optional invokeResponse.</returns> public override async Task <InvokeResponse> ForwardActivityAsync(ITurnContext turnContext, BotFrameworkSkill skill, Uri skillHostEndpoint, Activity activity, CancellationToken cancellationToken) { _logger.LogInformation($"Received request to forward activity to skill id {skill.Id}."); // Pull the current claims identity from TurnState (it is stored there on the way in). var identity = (ClaimsIdentity)turnContext.TurnState.Get <IIdentity>(BotIdentityKey); if (identity.AuthenticationType.Equals("anonymous", StringComparison.InvariantCultureIgnoreCase)) { throw new NotSupportedException("Anonymous calls are not supported for skills, please ensure your bot is configured with a MicrosoftAppId and Password)."); } // Get current Bot ID from the identity audience claim var botAppId = identity.Claims?.SingleOrDefault(claim => claim.Type == AuthenticationConstants.AudienceClaim)?.Value; if (string.IsNullOrWhiteSpace(botAppId)) { throw new InvalidOperationException("Unable to get the audience from the current request identity"); } var appCredentials = await GetAppCredentialsAsync(botAppId, skill.AppId).ConfigureAwait(false); if (appCredentials == null) { throw new InvalidOperationException("Unable to get appCredentials to connect to the skill"); } // Get token for the skill call var token = await appCredentials.GetTokenAsync().ConfigureAwait(false); // POST to skill using (var client = new HttpClient()) { // Create a deep clone of the activity so we can update it without impacting the original activity. var activityClone = JObject.FromObject(activity).ToObject <Activity>(); // TODO use SkillConversation class here instead of hard coded encoding... // Encode original bot service URL and ConversationId in the new conversation ID so we can unpack it later. // var skillConversation = new SkillConversation() { ServiceUrl = activity.ServiceUrl, ConversationId = activity.Conversation.Id }; // activity.Conversation.Id = skillConversation.GetSkillConversationId() activityClone.Conversation.Id = Convert.ToBase64String(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(new[] { activityClone.Conversation.Id, activityClone.ServiceUrl }))); activityClone.ServiceUrl = skillHostEndpoint.ToString(); activityClone.Recipient.Properties["skillId"] = skill.Id; using (var jsonContent = new StringContent(JsonConvert.SerializeObject(activityClone, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }), Encoding.UTF8, "application/json")) { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var response = await client.PostAsync($"{skill.SkillEndpoint}", jsonContent, cancellationToken).ConfigureAwait(false); var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false); if (content.Length > 0) { return(new InvokeResponse() { Status = (int)response.StatusCode, Body = JsonConvert.DeserializeObject(content) }); } } } return(null); }