/// <summary> /// Process incoming activity. This could be a message coming in from a channel (e.g. WebChat) or an /// agent reply coming RingCentral. /// </summary> /// <param name="turnContext"> The context object for this turn.</param> /// <param name="next">The delegate to call to continue the bot middleware pipeline.</param> /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param> /// <returns>A task that represents the work queued to execute.</returns> /// <remarks> /// Middleware calls the next delegate to pass control to the next middleware in /// the pipeline. If middleware doesn’t call the next delegate, the adapter does /// not call any of the subsequent middleware’s request handlers or the bot’s receive /// handler, and the pipeline short circuits. /// The <paramref name="turnContext"/> provides information about the incoming activity, and other data /// needed to process the activity. /// </remarks> public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default) { bool isFromRingCentral = MessageFromRingCentralOperator(turnContext.Activity); if (turnContext.Activity.Type == ActivityTypes.Message && !isFromRingCentral) { await _ringCentralClient.SendActivityToRingCentralAsync(turnContext.Activity).ConfigureAwait(false); var handoffRequestState = await _handoffRequestRecognizer.RecognizeHandoffRequestAsync(turnContext.Activity).ConfigureAwait(false); if (handoffRequestState != HandoffTarget.None) { string foreignThreadId = RingCentralSdkHelper.BuildForeignThreadIdFromActivity(turnContext.Activity); var thread = await _ringCentralClient.GetThreadByForeignThreadIdAsync(foreignThreadId).ConfigureAwait(false); if (thread != null) { await _ringCentralClient.HandoffConversationControlToAsync(handoffRequestState, thread).ConfigureAwait(false); await turnContext.SendActivityAsync($"Transfer to {handoffRequestState.ToString()} has been initiated.").ConfigureAwait(false); if (handoffRequestState == HandoffTarget.Bot) { return; } } else { _logger.LogWarning("Could not handoff the conversation, thread with foreign id \"{ForeignThreadId}\"could not be found.", foreignThreadId); } } } // Hook on messages that are sent from the bot to the user turnContext.OnSendActivities(SendActivitiesToUserHook); await next(cancellationToken).ConfigureAwait(false); }
/// <summary> /// Resolves the IActivity from the RingCentral payload, will send appropriate response back to RingCentral and return the type of RingCentral event to handle /// A single webhook endpoint can handle multiple events from RingCentral - therefore need to resolve what type of event this is from the payload /// Messages can also be posted to the adapter from a RingCentral Custom Source - which can be configured to be the bot endpoint. This is used in the scenario /// of human handoff when a Intervention.Opened is when a human operator takes over a conversation (eg. "Engages"). /// </summary> /// <param name="adapter">RingCentral adapter.</param> /// <param name="botAdapter">Bot adapter.</param> /// <param name="request">HttpRequest from caller.</param> /// <param name="response">HttpResponse for caller.</param> /// <returns>Task.</returns> public async Task <Tuple <RingCentralHandledEvent, Activity> > GetActivityFromRingCentralRequestAsync(RingCentralAdapter adapter, IBotFrameworkHttpAdapter botAdapter, HttpRequest request, HttpResponse response) { _ = adapter ?? throw new ArgumentNullException(nameof(adapter)); _ = botAdapter ?? throw new ArgumentNullException(nameof(botAdapter)); _ = request ?? throw new ArgumentNullException(nameof(request)); _ = response ?? throw new ArgumentNullException(nameof(response)); var payloadType = await GetTypedRingCentralPayloadAsync(request.Body); switch (payloadType) { case RingCentralEngageEvent ringCentralEngageEvent: { var metadata = ringCentralEngageEvent.Events.FirstOrDefault()?.Resource?.Metadata; if (ringCentralEngageEvent.Events.FirstOrDefault().Type.Equals(RingCentralEventDescription.ContentImported, StringComparison.InvariantCultureIgnoreCase)) { var newMessageActivity = await GetActivityFromRingCentralEventAsync(ringCentralEngageEvent, response); if (newMessageActivity == null) { break; } var handoffRequestStatus = await _handoffRequestRecognizer.RecognizeHandoffRequestAsync(newMessageActivity); // Bot requsted or bot in charge (not agent), return an activity if (handoffRequestStatus == HandoffTarget.Bot || metadata.CategoryIds.Contains(_options.CurrentValue.RingCentralEngageBotControlledThreadCategoryId, StringComparer.OrdinalIgnoreCase)) { return(new Tuple <RingCentralHandledEvent, Activity>(RingCentralHandledEvent.ContentImported, newMessageActivity)); } } break; } case RingCentralEngageAction ringCentralAction: { switch (ringCentralAction.Action) { case RingCentralEventDescription.MessageCreate: { var conversationRef = RingCentralSdkHelper.ConversationReferenceFromForeignThread(ringCentralAction.Params?.ThreadId, _options.CurrentValue.BotId); var humanActivity = RingCentralSdkHelper.RingCentralAgentResponseActivity(ringCentralAction.Params?.Body); _logger.LogTrace($"GetActivityFromRingCentralRequestAsync: ForeignThreadId: {ringCentralAction.Params?.ThreadId}, ConversationId: {conversationRef.Conversation.Id}, ChannelId: {conversationRef.ChannelId}, ServiceUrl: {conversationRef.ServiceUrl}"); // Use the botAdapter to send this agent (proactive) message through to the end user await((IAdapterIntegration)botAdapter).ContinueConversationAsync( _options.CurrentValue.MicrosoftAppId, conversationRef, async(ITurnContext turnContext, CancellationToken cancellationToken) => { MicrosoftAppCredentials.TrustServiceUrl(conversationRef.ServiceUrl); await turnContext.SendActivityAsync(humanActivity); }, default); object res = new { id = ringCentralAction.Params.InReplyToId, body = ringCentralAction.Params.Body }; var rbody = JsonSerializer.Serialize(res); response.StatusCode = (int)HttpStatusCode.OK; await response.WriteAsync(rbody); return(new Tuple <RingCentralHandledEvent, Activity>(RingCentralHandledEvent.Action, humanActivity)); } case RingCentralEventDescription.ImplementationInfo: { // Return implementation info response // https://github.com/ringcentral/engage-digital-source-sdk/wiki/Request-Response // https://github.com/ringcentral/engage-digital-source-sdk/wiki/Actions-details response.StatusCode = (int)HttpStatusCode.OK; var implementationResponse = RingCentralSdkHelper.ImplementationInfoResponse(); var rbody = JsonSerializer.SerializeToUtf8Bytes(implementationResponse); response.ContentType = "application/json"; await response.Body.WriteAsync(rbody); return(new Tuple <RingCentralHandledEvent, Activity>(RingCentralHandledEvent.Action, null)); } case RingCentralEventDescription.MessageList: case RingCentralEventDescription.PrivateMessagesList: case RingCentralEventDescription.ThreadsList: { response.StatusCode = (int)HttpStatusCode.OK; return(new Tuple <RingCentralHandledEvent, Activity>(RingCentralHandledEvent.Action, null)); } case RingCentralEventDescription.PrivateMessagesShow: case RingCentralEventDescription.ThreadsShow: { response.StatusCode = (int)HttpStatusCode.OK; return(new Tuple <RingCentralHandledEvent, Activity>(RingCentralHandledEvent.Action, null)); } default: break; } } break; default: break; } return(new Tuple <RingCentralHandledEvent, Activity>(RingCentralHandledEvent.Unknown, null)); }
/// <summary> /// This method can be called from inside a POST method on any controller implementation. /// It handles RingCentral webhooks of different types. /// </summary> /// <param name="httpRequest">The HTTP request object, typically in a POST handler by a controller.</param> /// <param name="httpResponse">When this method completes, the HTTP response to send.</param> /// <param name="bot">The bot that will handle the incoming activity.</param> /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param> /// <returns>A task that represents the work queued to execute.</returns> /// <exception cref="ArgumentNullException">Either <paramref name="httpRequest"/>, <paramref name="httpResponse"/>, <paramref name="bot"/> is null.</exception> public async Task ProcessAsync( HttpRequest httpRequest, HttpResponse httpResponse, IBot bot, CancellationToken cancellationToken = default) { _ = httpRequest ?? throw new ArgumentNullException(nameof(httpRequest)); _ = httpResponse ?? throw new ArgumentNullException(nameof(httpResponse)); _ = bot ?? throw new ArgumentNullException(nameof(bot)); var(ringCentralRequestType, activity) = new Tuple <RingCentralHandledEvent, Activity>(RingCentralHandledEvent.VerifyWebhook, null); // CONSIDER: DetectRequestType method // CONSDIER: Should we prevent giving request and response out of hands here? (hard to know who and when this objects might change) if (httpRequest.Query != null && httpRequest.Query.ContainsKey("hub.mode")) { ringCentralRequestType = RingCentralHandledEvent.VerifyWebhook; } else { (ringCentralRequestType, activity) = await _ringCentralClient.GetActivityFromRingCentralRequestAsync(this, _botAdapter, httpRequest, httpResponse).ConfigureAwait(false); } switch (ringCentralRequestType) { case RingCentralHandledEvent.VerifyWebhook: await _ringCentralClient.VerifyWebhookAsync(httpRequest, httpResponse, cancellationToken).ConfigureAwait(false); break; case RingCentralHandledEvent.Intervention: case RingCentralHandledEvent.Action: { // Process human agent responses coming from RingCentral (Custom Source SDK) if (activity != null) { using var context = new TurnContext(this, activity); await RunPipelineAsync(context, bot.OnTurnAsync, cancellationToken).ConfigureAwait(false); } break; } case RingCentralHandledEvent.ContentImported: { // Process messages created by any subscribed RingCentral source configured using a Webhook if (activity != null) { var handoffRequestStatus = await _handoffRequestRecognizer.RecognizeHandoffRequestAsync(activity).ConfigureAwait(false); // Bot or Agent request -> Re-Categorize if (handoffRequestStatus != HandoffTarget.None) { var channelData = activity.GetChannelData <RingCentralChannelData>(); var threadId = channelData.ThreadId; var thread = await _ringCentralClient.GetThreadByIdAsync(threadId).ConfigureAwait(false); if (thread != null) { await _ringCentralClient.HandoffConversationControlToAsync(handoffRequestStatus, thread).ConfigureAwait(false); } else { _logger.LogWarning("Could not handoff the conversation, thread with thread id \"{ThreadId}\" could not be found.", threadId); } } // Bot or no specific request if (handoffRequestStatus != HandoffTarget.Agent) { using var context = new TurnContext(this, activity); await RunPipelineAsync(context, bot.OnTurnAsync, cancellationToken).ConfigureAwait(false); } } break; } case RingCentralHandledEvent.Unknown: default: _logger.LogWarning($"Unsupported RingCentral Webhook or payload: '{httpRequest.PathBase}'."); break; } }