Example #1
0
        /// <summary>
        /// Logs events based on incoming and outgoing activities using the <see cref="IBotTelemetryClient"/> interface.
        /// </summary>
        /// <param name="context">The context object for this turn.</param>
        /// <param name="nextTurn">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>
        /// <seealso cref="ITurnContext"/>
        /// <seealso cref="Bot.Schema.IActivity"/>
        public virtual async Task OnTurnAsync(ITurnContext context, NextDelegate nextTurn, CancellationToken cancellationToken)
        {
            BotAssert.ContextNotNull(context);

            // log incoming activity at beginning of turn
            if (context.Activity != null)
            {
                var activity = context.Activity;

                // Log Bot Message Received
                await OnReceiveActivityAsync(activity, cancellationToken).ConfigureAwait(false);
            }

            // hook up onSend pipeline
            context.OnSendActivities(async(ctx, activities, nextSend) =>
            {
                // run full pipeline
                var responses = await nextSend().ConfigureAwait(false);

                foreach (var activity in activities)
                {
                    await OnSendActivityAsync(activity, cancellationToken).ConfigureAwait(false);
                }

                return(responses);
            });

            // hook up update activity pipeline
            context.OnUpdateActivity(async(ctx, activity, nextUpdate) =>
            {
                // run full pipeline
                var response = await nextUpdate().ConfigureAwait(false);

                await OnUpdateActivityAsync(activity, cancellationToken).ConfigureAwait(false);

                return(response);
            });

            // hook up delete activity pipeline
            context.OnDeleteActivity(async(ctx, reference, nextDelete) =>
            {
                // run full pipeline
                await nextDelete().ConfigureAwait(false);

                var deleteActivity = new Activity
                {
                    Type = ActivityTypes.MessageDelete,
                    Id   = reference.ActivityId,
                }
                .ApplyConversationReference(reference, isIncoming: false)
                .AsMessageDeleteActivity();

                await OnDeleteActivityAsync((Activity)deleteActivity, cancellationToken).ConfigureAwait(false);
            });

            if (nextTurn != null)
            {
                await nextTurn(cancellationToken).ConfigureAwait(false);
            }
        }
Example #2
0
        async Task IMiddleware.OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken)
        {
            var(shouldForwardToApplication, shouldIntercept) = await InvokeInboundAsync(turnContext, turnContext.Activity.TraceActivity("ReceivedActivity", "Received Activity"), cancellationToken).ConfigureAwait(false);

            if (shouldIntercept)
            {
                turnContext.OnSendActivities(async(ctx, activities, nextSend) =>
                {
                    var traceActivities = activities.Select(a => a.Type == ActivityTypes.Trace ? a.CloneTraceActivity() : a.TraceActivity("SentActivity", "Sent Activity"));
                    await InvokeOutboundAsync(ctx, traceActivities, cancellationToken).ConfigureAwait(false);
                    return(await nextSend().ConfigureAwait(false));
                });

                turnContext.OnUpdateActivity(async(ctx, activity, nextUpdate) =>
                {
                    var traceActivity = activity.TraceActivity("MessageUpdate", "Updated Message");
                    await InvokeOutboundAsync(ctx, traceActivity, cancellationToken).ConfigureAwait(false);
                    return(await nextUpdate().ConfigureAwait(false));
                });

                turnContext.OnDeleteActivity(async(ctx, reference, nextDelete) =>
                {
                    var traceActivity = reference.TraceActivity();
                    await InvokeOutboundAsync(ctx, traceActivity, cancellationToken).ConfigureAwait(false);
                    await nextDelete().ConfigureAwait(false);
                });
            }

            if (shouldForwardToApplication)
            {
                try
                {
                    await next(cancellationToken).ConfigureAwait(false);
                }
                catch (Exception e)
                {
                    await InvokeTraceExceptionAsync(turnContext, e.TraceActivity(), cancellationToken).ConfigureAwait(false);

                    throw;
                }
            }

            if (shouldIntercept)
            {
                await InvokeTraceStateAsync(turnContext, cancellationToken).ConfigureAwait(false);
            }
        }
Example #3
0
            protected override async Task OnMessageActivityAsync(ITurnContext <IMessageActivity> turnContext, CancellationToken cancellationToken)
            {
                // touch every
                var activity  = turnContext.Activity;
                var adapter   = turnContext.Adapter;
                var turnState = turnContext.TurnState;
                var responsed = turnContext.Responded;

                turnContext.OnDeleteActivity((t, a, n) => Task.CompletedTask);
                turnContext.OnSendActivities((t, a, n) => Task.FromResult(new ResourceResponse[] { new ResourceResponse() }));
                turnContext.OnUpdateActivity((t, a, n) => Task.FromResult(new ResourceResponse()));
                await turnContext.DeleteActivityAsync(activity.GetConversationReference());

                await turnContext.SendActivityAsync(new Activity());

                await turnContext.SendActivitiesAsync(new IActivity[] { new Activity() });

                await turnContext.UpdateActivityAsync(new Activity());
            }
 public ITurnContext OnDeleteActivity(DeleteActivityHandler handler)
 {
     return(_adapter.OnDeleteActivity(handler));
 }
Example #5
0
 public ITurnContext OnDeleteActivity(DeleteActivityHandler handler)
 => _innerTurnContext.OnDeleteActivity(handler);
        /// <summary>
        /// Records incoming and outgoing activities to the Application Insights store.
        /// </summary>
        /// <param name="context">The context object for this turn.</param>
        /// <param name="nextTurn">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>
        /// <seealso cref="ITurnContext"/>
        /// <seealso cref="Bot.Schema.IActivity"/>
        public async Task OnTurnAsync(ITurnContext context, NextDelegate nextTurn, CancellationToken cancellationToken)
        {
            BotAssert.ContextNotNull(context);

            context.TurnState.Add(TelemetryLoggerMiddleware.AppInsightsServiceKey, _telemetryClient);

            // log incoming activity at beginning of turn
            if (context.Activity != null)
            {
                var activity = context.Activity;

                // Log the Application Insights Bot Message Received
                _telemetryClient.TrackEventEx(TelemetryLoggerConstants.BotMsgReceiveEvent, activity, null, this.FillReceiveEventProperties(activity));
            }

            // hook up onSend pipeline
            context.OnSendActivities(async(ctx, activities, nextSend) =>
            {
                // run full pipeline
                var responses = await nextSend().ConfigureAwait(false);

                foreach (var activity in activities)
                {
                    _telemetryClient.TrackEventEx(TelemetryLoggerConstants.BotMsgSendEvent, activity, null, this.FillSendEventProperties(activity));
                }

                return(responses);
            });

            // hook up update activity pipeline
            context.OnUpdateActivity(async(ctx, activity, nextUpdate) =>
            {
                // run full pipeline
                var response = await nextUpdate().ConfigureAwait(false);

                _telemetryClient.TrackEventEx(TelemetryLoggerConstants.BotMsgUpdateEvent, activity, null, this.FillUpdateEventProperties(activity));

                return(response);
            });

            // hook up delete activity pipeline
            context.OnDeleteActivity(async(ctx, reference, nextDelete) =>
            {
                // run full pipeline
                await nextDelete().ConfigureAwait(false);

                var deleteActivity = new Activity
                {
                    Type = ActivityTypes.MessageDelete,
                    Id   = reference.ActivityId,
                }
                .ApplyConversationReference(reference, isIncoming: false)
                .AsMessageDeleteActivity();

                _telemetryClient.TrackEventEx(TelemetryLoggerConstants.BotMsgDeleteEvent, deleteActivity as Activity, null, this.FillDeleteEventProperties(deleteActivity));
            });

            if (nextTurn != null)
            {
                await nextTurn(cancellationToken).ConfigureAwait(false);
            }
        }
Example #7
0
        /// <summary>
        /// Records incoming and outgoing activities to the conversation store.
        /// </summary>
        /// <param name="turnContext">The context object for this turn.</param>
        /// <param name="nextTurn">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>
        /// <seealso cref="ITurnContext"/>
        /// <seealso cref="Bot.Schema.IActivity"/>
        public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate nextTurn, CancellationToken cancellationToken)
        {
            var transcript = new Queue <IActivity>();

            // log incoming activity at beginning of turn
            if (turnContext.Activity != null)
            {
                turnContext.Activity.From ??= new ChannelAccount();

                if (string.IsNullOrEmpty((string)turnContext.Activity.From.Properties["role"]) && string.IsNullOrEmpty(turnContext.Activity.From.Role))
                {
                    turnContext.Activity.From.Role = RoleTypes.User;
                }

                // We should not log ContinueConversation events used by skills to initialize the middleware.
                if (!(turnContext.Activity.Type == ActivityTypes.Event && turnContext.Activity.Name == ActivityEventNames.ContinueConversation))
                {
                    LogActivity(transcript, CloneActivity(turnContext.Activity));
                }
            }

            // hook up onSend pipeline
            turnContext.OnSendActivities(async(ctx, activities, nextSend) =>
            {
                // run full pipeline
                var responses = await nextSend().ConfigureAwait(false);

                foreach (var activity in activities)
                {
                    LogActivity(transcript, CloneActivity(activity));
                }

                return(responses);
            });

            // hook up update activity pipeline
            turnContext.OnUpdateActivity(async(ctx, activity, nextUpdate) =>
            {
                // run full pipeline
                var response = await nextUpdate().ConfigureAwait(false);

                // add Message Update activity
                var updateActivity  = CloneActivity(activity);
                updateActivity.Type = ActivityTypes.MessageUpdate;
                LogActivity(transcript, updateActivity);
                return(response);
            });

            // hook up delete activity pipeline
            turnContext.OnDeleteActivity(async(ctx, reference, nextDelete) =>
            {
                // run full pipeline
                await nextDelete().ConfigureAwait(false);

                // add MessageDelete activity
                // log as MessageDelete activity
                var deleteActivity = new Activity
                {
                    Type = ActivityTypes.MessageDelete,
                    Id   = reference.ActivityId,
                }
                .ApplyConversationReference(reference, isIncoming: false)
                .AsMessageDeleteActivity();

                LogActivity(transcript, deleteActivity);
            });

            // process bot logic
            await nextTurn(cancellationToken).ConfigureAwait(false);

            // flush transcript at end of turn
            // NOTE: We are not awaiting this task by design, TryLogTranscriptAsync() observes all exceptions and we don't need to or want to block execution on the completion.
            _ = TryLogTranscriptAsync(_logger, transcript);
        }
        /// <summary>
        /// Records incoming and outgoing activities to the conversation store.
        /// </summary>
        /// <param name="turnContext">The context object for this turn.</param>
        /// <param name="nextTurn">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>
        /// <seealso cref="ITurnContext"/>
        /// <seealso cref="Bot.Schema.IActivity"/>
        public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate nextTurn, CancellationToken cancellationToken)
        {
            Queue <IActivity> transcript = new Queue <IActivity>();

            // log incoming activity at beginning of turn
            if (turnContext.Activity != null)
            {
                if (turnContext.Activity.From == null)
                {
                    turnContext.Activity.From = new ChannelAccount();
                }

                if (string.IsNullOrEmpty((string)turnContext.Activity.From.Properties["role"]))
                {
                    turnContext.Activity.From.Properties["role"] = "user";
                }

                LogActivity(transcript, CloneActivity(turnContext.Activity));
            }

            // hook up onSend pipeline
            turnContext.OnSendActivities(async(ctx, activities, nextSend) =>
            {
                // run full pipeline
                var responses = await nextSend().ConfigureAwait(false);

                foreach (var activity in activities)
                {
                    LogActivity(transcript, CloneActivity(activity));
                }

                return(responses);
            });

            // hook up update activity pipeline
            turnContext.OnUpdateActivity(async(ctx, activity, nextUpdate) =>
            {
                // run full pipeline
                var response = await nextUpdate().ConfigureAwait(false);

                // add Message Update activity
                var updateActivity  = CloneActivity(activity);
                updateActivity.Type = ActivityTypes.MessageUpdate;
                LogActivity(transcript, updateActivity);
                return(response);
            });

            // hook up delete activity pipeline
            turnContext.OnDeleteActivity(async(ctx, reference, nextDelete) =>
            {
                // run full pipeline
                await nextDelete().ConfigureAwait(false);

                // add MessageDelete activity
                // log as MessageDelete activity
                var deleteActivity = new Activity
                {
                    Type = ActivityTypes.MessageDelete,
                    Id   = reference.ActivityId,
                }
                .ApplyConversationReference(reference, isIncoming: false)
                .AsMessageDeleteActivity();

                LogActivity(transcript, deleteActivity);
            });

            // process bot logic
            await nextTurn(cancellationToken).ConfigureAwait(false);

            // flush transcript at end of turn
            while (transcript.Count > 0)
            {
                var activity = transcript.Dequeue();

                // As we are deliberately not using await, disable the associated warning.
#pragma warning disable 4014
                logger.LogActivityAsync(activity).ContinueWith(
                    task =>
                {
                    try
                    {
                        task.Wait();
                    }
                    catch (Exception err)
                    {
                        Trace.TraceError($"Transcript logActivity failed with {err}");
                    }
                },
                    cancellationToken);
#pragma warning restore 4014
            }
        }
Example #9
0
        /// <summary>
        /// initialization for middleware turn
        /// </summary>
        /// <param name="context"></param>
        /// <param name="nextTurn"></param>
        /// <param name="cancellationToken"></param>
        /// <returns></returns>
        public async Task OnTurn(ITurnContext context, NextDelegate nextTurn, CancellationToken cancellationToken)
        {
            // log incoming activity at beginning of turn
            if (context.Activity != null)
            {
                if (string.IsNullOrEmpty((string)context.Activity.From.Properties["role"]))
                {
                    context.Activity.From.Properties["role"] = "user";
                }

                LogActivity(CloneActivity(context.Activity));
            }

            // hook up onSend pipeline
            context.OnSendActivities(async(ctx, activities, nextSend) =>
            {
                // run full pipeline
                var responses = await nextSend().ConfigureAwait(false);

                foreach (var activity in activities)
                {
                    LogActivity(CloneActivity(activity));
                }

                return(responses);
            });

            // hook up update activity pipeline
            context.OnUpdateActivity(async(ctx, activity, nextUpdate) =>
            {
                // run full pipeline
                var response = await nextUpdate().ConfigureAwait(false);

                // add Message Update activity
                var updateActivity  = CloneActivity(activity);
                updateActivity.Type = ActivityTypes.MessageUpdate;
                LogActivity(updateActivity);
                return(response);
            });

            // hook up delete activity pipeline
            context.OnDeleteActivity(async(ctx, reference, nextDelete) =>
            {
                // run full pipeline
                await nextDelete().ConfigureAwait(false);

                // add MessageDelete activity
                // log as MessageDelete activity
                var deleteActivity = new Activity
                {
                    Type = ActivityTypes.MessageDelete,
                    Id   = reference.ActivityId,
                }
                .ApplyConversationReference(reference, isIncoming: false)
                .AsMessageDeleteActivity();

                LogActivity(deleteActivity);
            });

            // process bot logic
            await nextTurn(cancellationToken).ConfigureAwait(false);

            // flush transcript at end of turn
            while (transcript.Count > 0)
            {
                try
                {
                    var activity = transcript.Dequeue();
                    await logger.LogActivity(activity).ConfigureAwait(false);
                }
                catch (Exception err)
                {
                    System.Diagnostics.Trace.TraceError($"Transcript logActivity failed with {err}");
                }
            }
        }
        /// <summary>
        /// Records incoming and outgoing activities to the conversation store.
        /// </summary>
        /// <param name="turnContext">The context object for this turn.</param>
        /// <param name="nextTurn">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>
        /// <seealso cref="ITurnContext"/>
        /// <seealso cref="Bot.Schema.IActivity"/>
        public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate nextTurn, CancellationToken cancellationToken)
        {
            Queue <IActivity> transcript = new Queue <IActivity>();

            // log incoming activity at beginning of turn
            if (turnContext.Activity != null)
            {
                if (turnContext.Activity.From == null)
                {
                    turnContext.Activity.From = new ChannelAccount();
                }

                if (string.IsNullOrEmpty((string)turnContext.Activity.From.Properties["role"]))
                {
                    turnContext.Activity.From.Properties["role"] = "user";
                }

                LogActivity(transcript, CloneActivity(turnContext.Activity));
            }

            // hook up onSend pipeline
            turnContext.OnSendActivities(async(ctx, activities, nextSend) =>
            {
                // run full pipeline
                var responses = await nextSend().ConfigureAwait(false);

                foreach (var activity in activities)
                {
                    LogActivity(transcript, CloneActivity(activity));
                }

                return(responses);
            });

            // hook up update activity pipeline
            turnContext.OnUpdateActivity(async(ctx, activity, nextUpdate) =>
            {
                // run full pipeline
                var response = await nextUpdate().ConfigureAwait(false);

                // add Message Update activity
                var updateActivity  = CloneActivity(activity);
                updateActivity.Type = ActivityTypes.MessageUpdate;
                LogActivity(transcript, updateActivity);
                return(response);
            });

            // hook up delete activity pipeline
            turnContext.OnDeleteActivity(async(ctx, reference, nextDelete) =>
            {
                // run full pipeline
                await nextDelete().ConfigureAwait(false);

                // add MessageDelete activity
                // log as MessageDelete activity
                var deleteActivity = new Activity
                {
                    Type = ActivityTypes.MessageDelete,
                    Id   = reference.ActivityId,
                }
                .ApplyConversationReference(reference, isIncoming: false)
                .AsMessageDeleteActivity();

                LogActivity(transcript, deleteActivity);
            });

            // process bot logic
            await nextTurn(cancellationToken).ConfigureAwait(false);

            // flush transcript at end of turn
            var logTasks = new List <Task>();

            while (transcript.Count > 0)
            {
                // Process the queue and log all the activities in parallel.
                var activity = transcript.Dequeue();

                // Add the logging task to the list (we don't call await here, we await all the calls together later).
                logTasks.Add(TryLogActivityAsync(_logger, activity));
            }

            if (logTasks.Any())
            {
                // Wait for all the activities to be logged before continuing.
                await Task.WhenAll(logTasks).ConfigureAwait(false);
            }
        }
Example #11
0
        /// <summary>
        /// Records incoming and outgoing activities to the Application Insights store.
        /// </summary>
        /// <param name="context">The <see cref="ITurnContext"/> object for this turn.</param>
        /// <param name="nextTurn">The delegate to call to continue the bot middleware pipeline.</param>
        /// <param name="cancellationToken">A <see cref="CancellationToken"/> that can be used by other objects
        /// or threads to receive notice of cancellation.</param>
        /// <returns>A <see cref="Task"/> that represents the work queued to execute.</returns>
        /// <seealso cref="ITurnContext"/>
        /// <seealso cref="Bot.Schema.IActivity"/>
        public async Task OnTurnAsync(ITurnContext context, NextDelegate nextTurn, CancellationToken cancellationToken)
        {
            BotAssert.ContextNotNull(context);

            if (nextTurn == null)
            {
                throw new ArgumentNullException(nameof(nextTurn));
            }

            context.TurnState.Add(MyAppInsightsLoggerMiddleware.AppInsightsServiceKey, _telemetryClient);

            // log incoming activity at beginning of turn
            if (context.Activity != null)
            {
                var activity = context.Activity;

                // Context properties for App Insights
                if (!string.IsNullOrEmpty(activity.Conversation.Id))
                {
                    _telemetryClient.Context.Session.Id = activity.Conversation.Id;
                }

                if (!string.IsNullOrEmpty(activity.From.Id))
                {
                    _telemetryClient.Context.User.Id = activity.From.Id;
                }

                // Log the Application Insights Bot Message Received
                _telemetryClient.TrackEvent(BotMsgReceiveEvent, FillReceiveEventProperties(activity));
            }

            // hook up onSend pipeline
            context.OnSendActivities(async(ctx, activities, nextSend) =>
            {
                // run full pipeline
                var responses = await nextSend().ConfigureAwait(false);

                foreach (var activity in activities)
                {
                    _telemetryClient.TrackEvent(BotMsgSendEvent, FillSendEventProperties(activity));
                }

                return(responses);
            });

            // hook up update activity pipeline
            context.OnUpdateActivity(async(ctx, activity, nextUpdate) =>
            {
                // run full pipeline
                var response = await nextUpdate().ConfigureAwait(false);

                _telemetryClient.TrackEvent(BotMsgUpdateEvent, FillUpdateEventProperties(activity));

                return(response);
            });

            // hook up delete activity pipeline
            context.OnDeleteActivity(async(ctx, reference, nextDelete) =>
            {
                // run full pipeline
                await nextDelete().ConfigureAwait(false);

                var deleteActivity = new Activity
                {
                    Type = ActivityTypes.MessageDelete,
                    Id   = reference.ActivityId,
                }
                .ApplyConversationReference(reference, isIncoming: false)
                .AsMessageDeleteActivity();

                _telemetryClient.TrackEvent(BotMsgDeleteEvent, FillDeleteEventProperties(deleteActivity));
            });

            if (nextTurn != null)
            {
                await nextTurn(cancellationToken).ConfigureAwait(false);
            }
        }
 public ITurnContext OnDeleteActivity(DeleteActivityHandler handler)
 {
     return(_innerTurnContext.OnDeleteActivity(handler));
 }
        public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default)
        {
            if (turnContext.Activity.Type == ActivityTypes.Event)
            {
                //If there was any previous conversation with the user,
                //we try to obtain history from storage provider and push it to newly opened conversation
                if (turnContext.Activity.Name == "webchat/join")
                {
                    var reference = turnContext.Activity.GetConversationReference();

                    //Create conversation id user id pair
                    _ucs.AddConvId(turnContext.Activity.From.Id, turnContext.Activity.Conversation.Id);

                    var pastActivities = _ucs.GetActivities(turnContext.Activity.From.Id);
                    if (pastActivities.Count > 0)
                    {
                        var connectorClient = turnContext.TurnState.Get <ConnectorClient>(typeof(IConnectorClient).FullName);

                        //We select only activities of type Message
                        var activities = pastActivities
                                         .Where(a => a.Type == ActivityTypes.Message)
                                         .Select(ia => (Activity)ia)
                                         .ToList();


                        // DirectLine only allows the upload of at most 500 activities at a time. The limit of 1500 below is
                        // arbitrary and up to the Bot author to decide.

                        var count = 0;
                        while (count < pastActivities.Count)
                        {
                            var take       = Math.Min(500, (activities.Count - count));
                            var transcript = new Transcript((activities.GetRange(count, take) as IList <Activity>));

                            //Thanks to channelData field activities will only get displayed in Web Chat Windows, which did not display them previously
                            await connectorClient.Conversations.SendConversationHistoryAsync(turnContext.Activity.Id, transcript, cancellationToken : cancellationToken);

                            count += 500;
                        }
                    }
                }
            }

            // log incoming activity at beginning of turn
            if (turnContext.Activity != null)
            {
                if (turnContext.Activity.From == null)
                {
                    turnContext.Activity.From = new ChannelAccount();
                }

                if (string.IsNullOrEmpty((string)turnContext.Activity.From.Properties["role"]))
                {
                    turnContext.Activity.From.Properties["role"] = "user";
                }

                LogActivity(CloneActivity(turnContext.Activity), turnContext.Activity.From.Id);
            }

            // hook up onSend pipeline
            turnContext.OnSendActivities(async(ctx, activities, nextSend) =>
            {
                // run full pipeline
                var responses = await nextSend().ConfigureAwait(false);

                foreach (var activity in activities)
                {
                    LogActivity(CloneActivity(activity), ctx.Activity.From.Id);
                }

                return(responses);
            });

            // hook up update activity pipeline
            turnContext.OnUpdateActivity(async(ctx, activity, nextUpdate) =>
            {
                // run full pipeline
                var response = await nextUpdate().ConfigureAwait(false);

                // add Message Update activity
                var updateActivity  = CloneActivity(activity);
                updateActivity.Type = ActivityTypes.MessageUpdate;
                LogActivity(updateActivity, ctx.Activity.From.Id);
                return(response);
            });

            // hook up delete activity pipeline
            turnContext.OnDeleteActivity(async(ctx, reference, nextDelete) =>
            {
                // run full pipeline
                await nextDelete().ConfigureAwait(false);

                // add MessageDelete activity
                // log as MessageDelete activity
                var deleteActivity = new Activity
                {
                    Type = ActivityTypes.MessageDelete,
                    Id   = reference.ActivityId,
                }
                .ApplyConversationReference(reference, isIncoming: false)
                .AsMessageDeleteActivity();

                LogActivity(deleteActivity, ctx.Activity.From.Id);
            });

            // process bot logic
            await next(cancellationToken).ConfigureAwait(false);
        }
        public async Task OnTurnAsync(ITurnContext turnContext, NextDelegate next, CancellationToken cancellationToken = default)
        {
            BotAssert.ContextNotNull(turnContext);

            if (next is null)
            {
                throw new ArgumentNullException(nameof(next));
            }

            turnContext.TurnState.Add(new CardManagerTurnState());

            var options       = GetOptionsForChannel(turnContext.Activity.ChannelId);
            var shouldProceed = true;

            // Is this activity from a button?
            if (turnContext.GetIncomingActionData() is JObject data)
            {
                var incomingIds    = data.GetIdsFromActionData();
                var autoDeactivate = data.GetLibraryValueFromActionData <string>(Behaviors.AutoDeactivate);

                // Actions should not be disabled if they have no data ID's
                if (incomingIds.Count() > 0)
                {
                    if (options.IdTrackingStyle != TrackingStyle.None && autoDeactivate != BehaviorSwitch.Off)
                    {
                        // Whether we should proceed by default depends on the ID-tracking style
                        shouldProceed = options.IdTrackingStyle == TrackingStyle.TrackDisabled;

                        var state = await Manager.GetStateAsync(turnContext, cancellationToken).ConfigureAwait(false);

                        foreach (var incomingId in incomingIds)
                        {
                            state.DataIdsByScope.TryGetValue(incomingId.Key, out var trackedSet);

                            var setContainsId = trackedSet?.Contains(incomingId.Value) == true;

                            if (setContainsId)
                            {
                                // Proceed if the presence of the ID indicates that the ID is enabled (opt-in logic),
                                // short-circuit if the presence of the ID indicates that the ID is disabled (opt-out logic)
                                shouldProceed = options.IdTrackingStyle == TrackingStyle.TrackEnabled;
                            }

                            if (options.AutoDisableOnAction || autoDeactivate == BehaviorSwitch.On)
                            {
                                // This might disable an already-disabled ID but that's okay
                                await Manager.DisableIdAsync(
                                    turnContext,
                                    incomingId,
                                    options.IdTrackingStyle,
                                    cancellationToken).ConfigureAwait(false);
                            }
                        }
                    }

                    if ((options.AutoDeleteOnAction && autoDeactivate != BehaviorSwitch.Off) ||
                        (options == UpdatingOptions && autoDeactivate == BehaviorSwitch.On))
                    {
                        // If there are multiple ID scopes in use, just delete the one with the largest range
                        var scope = DataId.Scopes.ElementAtOrDefault(incomingIds.Max(id => DataId.Scopes.IndexOf(id.Key)));

                        await Manager.DeleteActionSourceAsync(turnContext, scope, cancellationToken).ConfigureAwait(false);
                    }
                }
            }

            turnContext.OnSendActivities(OnSendActivities);
            turnContext.OnUpdateActivity(OnUpdateActivity);
            turnContext.OnDeleteActivity(OnDeleteActivity);

            if (shouldProceed)
            {
                // If this is not called, the middleware chain is effectively "short-circuited"
                await next(cancellationToken).ConfigureAwait(false);
            }
        }