// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.Bot.Schema; namespace Microsoft.Bot.Builder { /// /// Provides context for a turn of a bot. /// /// Context provides information needed to process an incoming activity. /// The context object is created by a and persists for the /// length of the turn. /// /// public class TurnContext : ITurnContext, IDisposable { private readonly IList _onSendActivities = new List(); private readonly IList _onUpdateActivity = new List(); private readonly IList _onDeleteActivity = new List(); /// /// Initializes a new instance of the class. /// /// The adapter creating the context. /// The incoming activity for the turn; /// or null for a turn for a proactive message. /// or /// is null. /// For use by bot adapter implementations only. public TurnContext(BotAdapter adapter, Activity activity) { Adapter = adapter ?? throw new ArgumentNullException(nameof(adapter)); Activity = activity ?? throw new ArgumentNullException(nameof(activity)); } /// /// Gets the bot adapter that created this context object. /// /// The bot adapter that created this context object. public BotAdapter Adapter { get; } /// /// Gets the services registered on this context object. /// /// The services registered on this context object. public TurnContextServiceCollection Services { get; } = new TurnContextServiceCollection(); /// /// Gets the activity associated with this turn; or null when processing /// a proactive message. /// /// The activity associated with this turn. public Activity Activity { get; } /// /// Gets a value indicating whether at least one response was sent for the current turn. /// /// true if at least one response was sent for the current turn. /// activities on their own do not set this flag. public bool Responded { get; private set; } /// /// Adds a response handler for send activity operations. /// /// The handler to add to the context object. /// The updated context object. /// is null. /// When the context's /// or methods are called, /// the adapter calls the registered handlers in the order in which they were /// added to the context object. /// public ITurnContext OnSendActivities(SendActivitiesHandler handler) { if (handler == null) { throw new ArgumentNullException(nameof(handler)); } _onSendActivities.Add(handler); return this; } /// /// Adds a response handler for update activity operations. /// /// The handler to add to the context object. /// The updated context object. /// is null. /// When the context's is called, /// the adapter calls the registered handlers in the order in which they were /// added to the context object. /// public ITurnContext OnUpdateActivity(UpdateActivityHandler handler) { if (handler == null) { throw new ArgumentNullException(nameof(handler)); } _onUpdateActivity.Add(handler); return this; } /// /// Adds a response handler for delete activity operations. /// /// The handler to add to the context object. /// The updated context object. /// is null. /// When the context's /// or is called, /// the adapter calls the registered handlers in the order in which they were /// added to the context object. /// public ITurnContext OnDeleteActivity(DeleteActivityHandler handler) { if (handler == null) { throw new ArgumentNullException(nameof(handler)); } _onDeleteActivity.Add(handler); return this; } /// /// Sends a message activity to the sender of the incoming activity. /// /// The text of the message to send. /// Optional, text to be spoken by your bot on a speech-enabled /// channel. /// Optional, indicates whether your bot is accepting, /// expecting, or ignoring user input after the message is delivered to the client. /// One of: "acceptingInput", "ignoringInput", or "expectingInput". /// Default is null. /// The cancellation token. /// A task that represents the work queued to execute. /// /// is null or whitespace. /// If the activity is successfully sent, the task result contains /// a object containing the ID that the receiving /// channel assigned to the activity. /// See the channel's documentation for limits imposed upon the contents of /// . /// To control various characteristics of your bot's speech such as voice, /// rate, volume, pronunciation, and pitch, specify in /// Speech Synthesis Markup Language (SSML) format. /// public async Task SendActivityAsync(string textReplyToSend, string speak = null, string inputHint = null, CancellationToken cancellationToken = default(CancellationToken)) { if (string.IsNullOrWhiteSpace(textReplyToSend)) { throw new ArgumentNullException(nameof(textReplyToSend)); } var activityToSend = new Activity(ActivityTypes.Message) { Text = textReplyToSend }; if (!string.IsNullOrEmpty(speak)) { activityToSend.Speak = speak; } if (!string.IsNullOrEmpty(inputHint)) { activityToSend.InputHint = inputHint; } return await SendActivityAsync(activityToSend, cancellationToken).ConfigureAwait(false); } /// /// Sends an activity to the sender of the incoming activity. /// /// The activity to send. /// Cancellation token. /// A task that represents the work queued to execute. /// is null. /// If the activity is successfully sent, the task result contains /// a object containing the ID that the receiving /// channel assigned to the activity. public async Task SendActivityAsync(IActivity activity, CancellationToken cancellationToken = default(CancellationToken)) { BotAssert.ActivityNotNull(activity); ResourceResponse[] responses = await SendActivitiesAsync(new[] { activity }, cancellationToken).ConfigureAwait(false); if (responses == null || responses.Length == 0) { // It's possible an interceptor prevented the activity from having been sent. // Just return an empty response in that case. return new ResourceResponse(); } else { return responses[0]; } } /// /// Sends a set of activities to the sender of the incoming activity. /// /// The activities to send. /// Cancellation token. /// A task that represents the work queued to execute. /// If the activities are successfully sent, the task result contains /// an array of objects containing the IDs that /// the receiving channel assigned to the activities. public Task SendActivitiesAsync(IActivity[] activities, CancellationToken cancellationToken = default(CancellationToken)) { if (activities == null) { throw new ArgumentNullException(nameof(activities)); } if (activities.Length == 0) { throw new ArgumentException("Expecting one or more activities, but the array was empty.", nameof(activities)); } var conversationReference = this.Activity.GetConversationReference(); var bufferedActivities = new List(activities.Length); for (var index = 0; index < activities.Length; index++) { // Buffer the incoming activities into a List since we allow the set to be manipulated by the callbacks // Bind the relevant Conversation Reference properties, such as URLs and // ChannelId's, to the activity we're about to send bufferedActivities.Add(activities[index].ApplyConversationReference(conversationReference)); } // If there are no callbacks registered, bypass the overhead of invoking them and send directly to the adapter if (_onSendActivities.Count == 0) { return SendActivitiesThroughAdapter(); } // Send through the full callback pipeline return SendActivitiesThroughCallbackPipeline(); Task SendActivitiesThroughCallbackPipeline(int nextCallbackIndex = 0) { // If we've executed the last callback, we now send straight to the adapter if (nextCallbackIndex == _onSendActivities.Count) { return SendActivitiesThroughAdapter(); } return _onSendActivities[nextCallbackIndex].Invoke(this, bufferedActivities, () => SendActivitiesThroughCallbackPipeline(nextCallbackIndex + 1)); } async Task SendActivitiesThroughAdapter() { // Send from the list which may have been manipulated via the event handlers. // Note that 'responses' was captured from the root of the call, and will be // returned to the original caller. var responses = await Adapter.SendActivitiesAsync(this, bufferedActivities.ToArray(), cancellationToken).ConfigureAwait(false); var sentNonTraceActivity = false; for (var index = 0; index < responses.Length; index++) { var activity = bufferedActivities[index]; activity.Id = responses[index].Id; sentNonTraceActivity |= activity.Type != ActivityTypes.Trace; } if (sentNonTraceActivity) { Responded = true; } return responses; } } /// /// Replaces an existing activity. /// /// New replacement activity. /// Cancellation token. /// A task that represents the work queued to execute. /// /// The HTTP operation failed and the response contained additional information. /// /// One or more exceptions occurred during the operation. /// If the activity is successfully sent, the task result contains /// a object containing the ID that the receiving /// channel assigned to the activity. /// Before calling this, set the ID of the replacement activity to the ID /// of the activity to replace. public async Task UpdateActivityAsync(IActivity activity, CancellationToken cancellationToken = default(CancellationToken)) { Activity a = (Activity)activity; async Task ActuallyUpdateStuff() { return await Adapter.UpdateActivityAsync(this, a, cancellationToken).ConfigureAwait(false); } return await UpdateActivityInternalAsync(a, _onUpdateActivity, ActuallyUpdateStuff, cancellationToken).ConfigureAwait(false); } /// /// Deletes an existing activity. /// /// The ID of the activity to delete. /// Cancellation token. /// A task that represents the work queued to execute. /// /// The HTTP operation failed and the response contained additional information. public async Task DeleteActivityAsync(string activityId, CancellationToken cancellationToken = default(CancellationToken)) { if (string.IsNullOrWhiteSpace(activityId)) { throw new ArgumentNullException(nameof(activityId)); } var cr = Activity.GetConversationReference(); cr.ActivityId = activityId; async Task ActuallyDeleteStuff() { await Adapter.DeleteActivityAsync(this, cr, cancellationToken).ConfigureAwait(false); } await DeleteActivityInternalAsync(cr, _onDeleteActivity, ActuallyDeleteStuff, cancellationToken).ConfigureAwait(false); } /// /// Deletes an existing activity. /// /// The conversation containing the activity to delete. /// Cancellation token. /// A task that represents the work queued to execute. /// /// The HTTP operation failed and the response contained additional information. /// The conversation reference's /// indicates the activity in the conversation to delete. public async Task DeleteActivityAsync(ConversationReference conversationReference, CancellationToken cancellationToken = default(CancellationToken)) { if (conversationReference == null) { throw new ArgumentNullException(nameof(conversationReference)); } async Task ActuallyDeleteStuff() { await Adapter.DeleteActivityAsync(this, conversationReference, cancellationToken).ConfigureAwait(false); } await DeleteActivityInternalAsync(conversationReference, _onDeleteActivity, ActuallyDeleteStuff, cancellationToken).ConfigureAwait(false); } public void Dispose() { Services.Dispose(); } private async Task UpdateActivityInternalAsync( Activity activity, IEnumerable updateHandlers, Func> callAtBottom, CancellationToken cancellationToken) { BotAssert.ActivityNotNull(activity); if (updateHandlers == null) { throw new ArgumentException(nameof(updateHandlers)); } // No middleware to run. if (updateHandlers.Count() == 0) { if (callAtBottom != null) { return await callAtBottom().ConfigureAwait(false); } return null; } // Default to "No more Middleware after this". async Task Next() { // Remove the first item from the list of middleware to call, // so that the next call just has the remaining items to worry about. IEnumerable remaining = updateHandlers.Skip(1); var result = await UpdateActivityInternalAsync(activity, remaining, callAtBottom, cancellationToken).ConfigureAwait(false); activity.Id = result.Id; return result; } // Grab the current middleware, which is the 1st element in the array, and execute it UpdateActivityHandler toCall = updateHandlers.First(); return await toCall(this, activity, Next).ConfigureAwait(false); } private async Task DeleteActivityInternalAsync( ConversationReference cr, IEnumerable updateHandlers, Func callAtBottom, CancellationToken cancellationToken) { BotAssert.ConversationReferenceNotNull(cr); if (updateHandlers == null) { throw new ArgumentException(nameof(updateHandlers)); } // No middleware to run. if (updateHandlers.Count() == 0) { if (callAtBottom != null) { await callAtBottom().ConfigureAwait(false); } return; } // Default to "No more Middleware after this". async Task Next() { // Remove the first item from the list of middleware to call, // so that the next call just has the remaining items to worry about. IEnumerable remaining = updateHandlers.Skip(1); await DeleteActivityInternalAsync(cr, remaining, callAtBottom, cancellationToken).ConfigureAwait(false); } // Grab the current middleware, which is the 1st element in the array, and execute it. DeleteActivityHandler toCall = updateHandlers.First(); await toCall(this, cr, Next).ConfigureAwait(false); } } }