public async Task ResponseExtensions_SetBodyString_Success() { var r = new StreamingResponse(); r.SetBody("123"); Assert.NotNull(r.Streams); Assert.Single(r.Streams); Assert.Equal(typeof(StringContent), r.Streams[0].Content.GetType()); var s = await r.Streams[0].Content.ReadAsStringAsync().ConfigureAwait(false); Assert.Equal("123", s); }
public void ResponseExtensions_SetBody_Null_Does_Not_Throw() { var r = new StreamingResponse(); Exception ex = null; try { r.SetBody(null); } catch (Exception caughtEx) { ex = caughtEx; } finally { Assert.Null(ex); } }
public async Task ResponseExtensions_SetBody_Success() { var r = new StreamingResponse(); var a = new Activity() { Text = "hi", Type = "message" }; r.SetBody(a); Assert.NotNull(r.Streams); Assert.Single(r.Streams); Assert.Equal(typeof(StringContent), r.Streams[0].Content.GetType()); var s = JsonConvert.DeserializeObject <Activity>(await r.Streams[0].Content.ReadAsStringAsync().ConfigureAwait(false)); Assert.Equal(a.Text, s.Text); Assert.Equal(a.Type, s.Type); }
public async Task ResponseExtensions_SetBody_Success() { var r = new StreamingResponse(); var a = new Activity() { Text = "hi", Type = "message" }; r.SetBody(a); Assert.IsNotNull(r.Streams); Assert.AreEqual(1, r.Streams.Count); Assert.AreEqual(typeof(StringContent), r.Streams[0].Content.GetType()); var s = await r.Streams[0].Content.ReadAsAsync <Activity>().ConfigureAwait(false); Assert.AreEqual(a.Text, s.Text); Assert.AreEqual(a.Type, s.Type); }
/// <summary> /// Checks the validity of the request and attempts to map it the correct custom endpoint, /// then generates and returns a response if appropriate. /// </summary> /// <param name="request">A ReceiveRequest from the connected channel.</param> /// <param name="response">The <see cref="StreamingResponse"/> instance.</param> /// <returns>A response if the given request matches against a defined path.</returns> private StreamingResponse HandleCustomPaths(ReceiveRequest request, StreamingResponse response) { if (request == null || string.IsNullOrEmpty(request.Verb) || string.IsNullOrEmpty(request.Path)) { response.StatusCode = (int)HttpStatusCode.BadRequest; _logger.LogError("Request missing verb and/or path."); return(response); } if (string.Equals(request.Verb, StreamingRequest.GET, StringComparison.OrdinalIgnoreCase) && string.Equals(request.Path, "/api/version", StringComparison.OrdinalIgnoreCase)) { response.StatusCode = (int)HttpStatusCode.OK; response.SetBody(new VersionInfo() { UserAgent = _userAgent }); return(response); } return(null); }
/// <summary> /// Checks the validity of the request and attempts to map it the correct virtual endpoint, /// then generates and returns a response if appropriate. /// </summary> /// <param name="request">A ReceiveRequest from the connected channel.</param> /// <param name="logger">Optional logger used to log request information and error details.</param> /// <param name="context">Optional context to operate within. Unused in bot implementation.</param> /// /// <param name="cancellationToken">Optional cancellation token.</param> /// <returns>A response created by the BotAdapter to be sent to the client that originated the request.</returns> public override async Task <StreamingResponse> ProcessRequestAsync(ReceiveRequest request, ILogger <RequestHandler> logger, object context = null, CancellationToken cancellationToken = default(CancellationToken)) { logger = logger ?? NullLogger <RequestHandler> .Instance; var response = new StreamingResponse(); if (request == null || string.IsNullOrEmpty(request.Verb) || string.IsNullOrEmpty(request.Path)) { response.StatusCode = (int)HttpStatusCode.BadRequest; logger.LogError("Request missing verb and/or path."); return(response); } if (string.Equals(request.Verb, StreamingRequest.GET, StringComparison.InvariantCultureIgnoreCase) && string.Equals(request.Path, "/api/version", StringComparison.InvariantCultureIgnoreCase)) { response.StatusCode = (int)HttpStatusCode.OK; response.SetBody(new VersionInfo() { UserAgent = _userAgent }); return(response); } if (string.Equals(request.Verb, StreamingRequest.POST, StringComparison.InvariantCultureIgnoreCase) && string.Equals(request.Path, "/api/messages", StringComparison.InvariantCultureIgnoreCase)) { return(await ProcessStreamingRequestAsync(request, response, logger, cancellationToken).ConfigureAwait(false)); } response.StatusCode = (int)HttpStatusCode.NotFound; logger.LogError($"Unknown verb and path: {request.Verb} {request.Path}"); return(response); }
/// <summary> /// Handles incoming requests. /// </summary> /// <param name="request">A <see cref="ReceiveRequest"/> for this handler to process.</param> /// <param name="logger">Logger.</param> /// <param name="context">Optional context to process the request within.</param> /// <param name="cancellationToken">Cancellation token.</param> /// <returns>A <see cref="Task"/> that will produce a <see cref="StreamingResponse"/> on successful completion.</returns> public override async Task <StreamingResponse> ProcessRequestAsync(ReceiveRequest request, ILogger <RequestHandler> logger = null, object context = null, CancellationToken cancellationToken = default) { var response = new StreamingResponse(); // We accept all POSTs regardless of path, but anything else requires special treatment. if (!string.Equals(request?.Verb, StreamingRequest.POST, StringComparison.OrdinalIgnoreCase)) { return(HandleCustomPaths(request, response)); } // Convert the StreamingRequest into an activity the adapter can understand. string body; try { body = request.ReadBodyAsString(); } #pragma warning disable CA1031 // Do not catch general exception types (we log the exception and continue execution) catch (Exception ex) #pragma warning restore CA1031 // Do not catch general exception types { response.StatusCode = (int)HttpStatusCode.BadRequest; _logger.LogError("Request body missing or malformed: " + ex.Message); return(response); } try { var activity = JsonConvert.DeserializeObject <Activity>(body, SerializationSettings.DefaultDeserializationSettings); // All activities received by this StreamingRequestHandler will originate from the same channel, but we won't // know what that channel is until we've received the first request. if (string.IsNullOrWhiteSpace(ServiceUrl)) { ServiceUrl = activity.ServiceUrl; } // If this is the first time the handler has seen this conversation it needs to be added to the dictionary so the // adapter is able to route requests to the correct handler. if (!HasConversation(activity.Conversation.Id)) { _conversations.Add(activity.Conversation.Id, DateTime.Now); } /* * Any content sent as part of a StreamingRequest, including the request body * and inline attachments, appear as streams added to the same collection. The first * stream of any request will be the body, which is parsed and passed into this method * as the first argument, 'body'. Any additional streams are inline attachments that need * to be iterated over and added to the Activity as attachments to be sent to the Bot. */ if (request.Streams.Count > 1) { var streamAttachments = new List <Attachment>(); for (var i = 1; i < request.Streams.Count; i++) { streamAttachments.Add(new Attachment() { ContentType = request.Streams[i].ContentType, Content = request.Streams[i].Stream }); } if (activity.Attachments != null) { activity.Attachments = activity.Attachments.Concat(streamAttachments).ToArray(); } else { activity.Attachments = streamAttachments.ToArray(); } } // Now that the request has been converted into an activity we can send it to the adapter. var adapterResponse = await _activityProcessor.ProcessStreamingActivityAsync(activity, _bot.OnTurnAsync, cancellationToken).ConfigureAwait(false); // Now we convert the invokeResponse returned by the adapter into a StreamingResponse we can send back to the channel. if (adapterResponse == null) { response.StatusCode = (int)HttpStatusCode.OK; } else { response.StatusCode = adapterResponse.Status; if (adapterResponse.Body != null) { response.SetBody(adapterResponse.Body); } } } #pragma warning disable CA1031 // Do not catch general exception types (we logging the error and we send it back in the body of the response) catch (Exception ex) #pragma warning restore CA1031 // Do not catch general exception types { response.StatusCode = (int)HttpStatusCode.InternalServerError; response.SetBody(ex.ToString()); _logger.LogError(ex.ToString()); } return(response); }
/// <summary> /// Handles incoming requests. /// </summary> /// <param name="request">A <see cref="ReceiveRequest"/> for this handler to process.</param> /// <param name="logger">Logger.</param> /// <param name="context">Optional context to process the request within.</param> /// <param name="cancellationToken">Cancellation token.</param> /// <returns>A <see cref="Task"/> that will produce a <see cref="StreamingResponse"/> on successful completion.</returns> public override async Task <StreamingResponse> ProcessRequestAsync(ReceiveRequest request, ILogger <RequestHandler> logger = null, object context = null, CancellationToken cancellationToken = default) { var response = new StreamingResponse(); // We accept all POSTs regardless of path, but anything else requires special treatment. if (!string.Equals(request?.Verb, StreamingRequest.POST, StringComparison.OrdinalIgnoreCase)) { return(HandleCustomPaths(request, response)); } // Convert the StreamingRequest into an activity the adapter can understand. string body; try { body = await request.ReadBodyAsStringAsync().ConfigureAwait(false); } #pragma warning disable CA1031 // Do not catch general exception types (we log the exception and continue execution) catch (Exception ex) #pragma warning restore CA1031 // Do not catch general exception types { response.StatusCode = (int)HttpStatusCode.BadRequest; _logger.LogError("Request body missing or malformed: " + ex.Message); return(response); } try { var activity = JsonConvert.DeserializeObject <Activity>(body, SerializationSettings.DefaultDeserializationSettings); // All activities received by this StreamingRequestHandler will originate from the same channel, but we won't // know what that channel is until we've received the first request. if (string.IsNullOrWhiteSpace(ServiceUrl)) { ServiceUrl = activity.ServiceUrl; } // If this is the first time the handler has seen this conversation it needs to be added to the dictionary so the // adapter is able to route requests to the correct handler. if (!HasConversation(activity.Conversation.Id)) { _conversations.Add(activity.Conversation.Id, DateTime.Now); } /* * Any content sent as part of a StreamingRequest, including the request body * and inline attachments, appear as streams added to the same collection. The first * stream of any request will be the body, which is parsed and passed into this method * as the first argument, 'body'. Any additional streams are inline attachments that need * to be iterated over and added to the Activity as attachments to be sent to the Bot. */ if (request.Streams.Count > 1) { var streamAttachments = new List <Attachment>(); for (var i = 1; i < request.Streams.Count; i++) { streamAttachments.Add(new Attachment() { ContentType = request.Streams[i].ContentType, Content = request.Streams[i].Stream }); } if (activity.Attachments != null) { activity.Attachments = activity.Attachments.Concat(streamAttachments).ToArray(); } else { activity.Attachments = streamAttachments.ToArray(); } } // Populate Activity.CallerId given the Audience value. string callerId = null; switch (Audience) { case AuthenticationConstants.ToChannelFromBotOAuthScope: callerId = CallerIdConstants.PublicAzureChannel; break; case GovernmentAuthenticationConstants.ToChannelFromBotOAuthScope: callerId = CallerIdConstants.USGovChannel; break; default: if (!string.IsNullOrEmpty(Audience)) { if (Guid.TryParse(Audience, out var result)) { // Large assumption drawn here; any GUID is an AAD AppId. This is prohibitive towards bots not using the Bot Framework auth model // but still using GUIDs/UUIDs as identifiers. // It's also indicative of the tight coupling between the Bot Framework protocol, authentication and transport mechanism in the SDK. // In R12, this work will be re-implemented to better utilize the CallerId and Audience set on BotFrameworkAuthentication instances // and decouple the three concepts mentioned above. callerId = $"{CallerIdConstants.BotToBotPrefix}{Audience}"; } else { // Fallback to using the raw Audience as the CallerId. The auth model being used by the Adapter using this StreamingRequestHandler // is not known to the SDK, therefore it is assumed the developer knows what they're doing. The SDK should not prevent // the developer from extending it to use custom auth models in Streaming contexts. callerId = Audience; } } // A null Audience is an implicit statement indicating the bot does not support skills. break; } activity.CallerId = callerId; // Now that the request has been converted into an activity we can send it to the adapter. var adapterResponse = await _activityProcessor.ProcessStreamingActivityAsync(activity, _bot.OnTurnAsync, cancellationToken).ConfigureAwait(false); // Now we convert the invokeResponse returned by the adapter into a StreamingResponse we can send back to the channel. if (adapterResponse == null) { response.StatusCode = (int)HttpStatusCode.OK; } else { response.StatusCode = adapterResponse.Status; if (adapterResponse.Body != null) { response.SetBody(adapterResponse.Body); } } } #pragma warning disable CA1031 // Do not catch general exception types (we logging the error and we send it back in the body of the response) catch (Exception ex) #pragma warning restore CA1031 // Do not catch general exception types { response.StatusCode = (int)HttpStatusCode.InternalServerError; response.SetBody(ex.ToString()); _logger.LogError(ex.ToString()); } return(response); }
/// <summary> /// Performs the actual processing of a request, handing it off to the adapter and returning the response. /// </summary> /// <param name="request">A ReceiveRequest from the connected channel.</param> /// <param name="response">The response to update and return, ultimately sent to client.</param> /// <param name="logger">Optional logger.</param> /// <param name="cancellationToken">Cancellation token.</param> /// <returns>The response ready to send to the client.</returns> private async Task <StreamingResponse> ProcessStreamingRequestAsync(ReceiveRequest request, StreamingResponse response, ILogger <RequestHandler> logger, CancellationToken cancellationToken) { var body = string.Empty; try { body = request.ReadBodyAsString(); } catch (Exception ex) { response.StatusCode = (int)HttpStatusCode.BadRequest; logger.LogError("Request body missing or malformed: " + ex.Message); return(response); } try { var adapter = new BotFrameworkStreamingExtensionsAdapter(_transportServer, _middlewareSet, logger); IBot bot = null; // First check if an IBot type definition is available from the service provider. if (_services != null) { /* Creating a new scope for each request allows us to support * bots that inject scoped services as dependencies. */ bot = _services.CreateScope().ServiceProvider.GetService <IBot>(); } // If no bot has been set, check if a singleton bot is associated with this request handler. if (bot == null) { bot = _bot; } // If a bot still hasn't been set, the request will not be handled correctly, so throw and terminate. if (bot == null) { throw new Exception("Unable to find bot when processing request."); } adapter.OnTurnError = _onTurnError; var invokeResponse = await adapter.ProcessActivityAsync(body, request.Streams, new BotCallbackHandler(bot.OnTurnAsync), cancellationToken).ConfigureAwait(false); if (invokeResponse == null) { response.StatusCode = (int)HttpStatusCode.OK; } else { response.StatusCode = invokeResponse.Status; if (invokeResponse.Body != null) { response.SetBody(invokeResponse.Body); } } invokeResponse = null; } catch (Exception ex) { response.StatusCode = (int)HttpStatusCode.InternalServerError; logger.LogError(ex.Message); } return(response); }
public async override Task <StreamingResponse> ProcessRequestAsync(ReceiveRequest request, ILogger <RequestHandler> logger = null, object context = null, CancellationToken cancellationToken = default(CancellationToken)) { if (Bot == null) { throw new ArgumentNullException(nameof(Bot)); } if (SkillWebSocketBotAdapter == null) { throw new ArgumentNullException(nameof(SkillWebSocketBotAdapter)); } var response = new StreamingResponse(); var body = request.ReadBodyAsString(); if (string.IsNullOrEmpty(body) || request.Streams?.Count == 0) { response.StatusCode = (int)HttpStatusCode.BadRequest; response.SetBody("Empty request body."); return(response); } if (request.Streams.Where(x => x.ContentType != "application/json; charset=utf-8").Any()) { response.StatusCode = (int)HttpStatusCode.NotAcceptable; return(response); } Activity activity = null; try { activity = JsonConvert.DeserializeObject <Activity>(body, Serialization.Settings); } catch (Exception ex) { _botTelemetryClient.TrackException(ex); response.StatusCode = (int)HttpStatusCode.BadRequest; response.SetBody("Request body is not an Activity instance."); return(response); } if (_claimsIdentity.AuthenticationType != "anonymous") { var appIdClaimName = AuthHelpers.GetAppIdClaimName(_claimsIdentity); // retrieve the appid and use it to populate callerId on the activity activity.CallerId = _claimsIdentity.Claims.FirstOrDefault(c => c.Type == appIdClaimName)?.Value; } try { var cancellationTokenSource = new CancellationTokenSource(); _stopWatch.Start(); var invokeResponse = await this.SkillWebSocketBotAdapter.ProcessActivityAsync(activity, new BotCallbackHandler(this.Bot.OnTurnAsync), cancellationTokenSource.Token).ConfigureAwait(false); _stopWatch.Stop(); _botTelemetryClient.TrackEvent("SkillWebSocketProcessRequestLatency", null, new Dictionary <string, double> { { "Latency", _stopWatch.ElapsedMilliseconds }, }); // trigger cancel token after activity is handled. this will stop the typing indicator cancellationTokenSource.Cancel(); if (invokeResponse == null) { response.StatusCode = (int)HttpStatusCode.OK; } else { response.StatusCode = invokeResponse.Status; if (invokeResponse.Body != null) { response.SetBody(invokeResponse.Body); } } } catch (SkillWebSocketCallbackException ex) { _botTelemetryClient.TrackException(ex); response.StatusCode = (int)HttpStatusCode.InternalServerError; response.SetBody(ex.Message); return(response); } catch (Exception ex) { _botTelemetryClient.TrackException(ex); response.StatusCode = (int)HttpStatusCode.InternalServerError; } return(response); }