// 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);
}
}
}