// TODO: Incorporate value preservation into other updates made to Adaptive Cards
        public async Task PreserveValuesAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
        {
            BotAssert.ContextNotNull(turnContext);

            if (turnContext.GetIncomingActionData() is JObject data)
            {
                var matchResult = await GetDataMatchAsync(turnContext, cancellationToken).ConfigureAwait(false);

                if (matchResult.SavedActivity != null &&
                    matchResult.SavedAttachment?.ContentType.EqualsCI(ContentTypes.AdaptiveCard) == true)
                {
                    var changed = false;

                    // The content must be non-null or else the attachment couldn't have been a match
                    matchResult.SavedAttachment.Content = matchResult.SavedAttachment.Content.ToJObjectAndBack(
                        card =>
                    {
                        // Iterate through all inputs in the card
                        foreach (var input in AdaptiveCardUtil.GetAdaptiveInputs(card))
                        {
                            var id         = AdaptiveCardUtil.GetAdaptiveInputId(input);
                            var inputValue = data.GetValue(id);

                            input.SetValue(AdaptiveProperties.Value, inputValue);

                            changed = true;
                        }
                    });

                    if (changed)
                    {
                        // The changes to the attachment will already be reflected in the activity
                        await UpdateActivityAsync(turnContext, matchResult.SavedActivity, cancellationToken).ConfigureAwait(false);
                    }
                }
            }
        }
Example #2
0
        private async Task <DataMatchResult> GetDataMatchAsync(
            ITurnContext turnContext,
            CancellationToken cancellationToken = default)
        {
            var result = new DataMatchResult();

            if (!(turnContext.GetIncomingActionData() is JObject incomingData))
            {
                return(result);
            }

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

            var couldBeFromAdaptiveCard = turnContext.Activity.Value.ToJObject() is JObject;

            // Iterate over all saved activities that contain any of the action data ID's from the incoming action data
            foreach (var savedActivity in state.SavedActivities
                     .Where(activity => activity?.Attachments?.Any() == true))
            {
                foreach (var savedAttachment in savedActivity.Attachments.WhereNotNull())
                {
                    if (savedAttachment.ContentType.EqualsCI(ContentTypes.AdaptiveCard))
                    {
                        if (couldBeFromAdaptiveCard && savedAttachment.Content.ToJObject() is JObject savedAdaptiveCard)
                        {
                            // For Adaptive Cards we need to check the inputs
                            var inputsMatch            = true;
                            var dataWithoutInputValues = incomingData.DeepClone() as JObject;

                            // First, determine what matching submit action data is expected to look like
                            // by taking the incoming data and removing the values associated with
                            // the inputs found in the Adaptive Card
                            foreach (var inputId in AdaptiveCardUtil.GetAdaptiveInputs(savedAdaptiveCard)
                                     .Select(AdaptiveCardUtil.GetAdaptiveInputId))
                            {
                                // If the Adaptive Card is poorly designed,
                                // the same input ID might show up multiple times.
                                // Therefore we're checking if the original incoming data
                                // contained the ID, because the inputs might still
                                // match even if this input was already removed.
                                if (incomingData.ContainsKey(inputId))
                                {
                                    // Removing a property that doesn't exist
                                    // will not throw an exception
                                    dataWithoutInputValues.Remove(inputId);
                                }
                                else
                                {
                                    inputsMatch = false;

                                    break;
                                }
                            }

                            // Second, if all the input ID's found in the card were present in the incoming data
                            // then check each submit action in the card to see if its data matches the incoming data
                            if (inputsMatch)
                            {
                                CardTree.Recurse(
                                    savedAdaptiveCard,
                                    (JObject savedSubmitAction) =>
                                {
                                    var submitActionData = savedSubmitAction.GetValue(
                                        AdaptiveProperties.Data) ?? new JObject();

                                    if (JToken.DeepEquals(submitActionData, dataWithoutInputValues))
                                    {
                                        result.Add(savedActivity, savedAttachment, savedSubmitAction);
                                    }
                                },
                                    TreeNodeType.AdaptiveCard,
                                    TreeNodeType.SubmitAction);
                            }
                        }
                    }
                    else
                    {
                        // For Bot Framework cards that are not Adaptive Cards
                        CardTree.Recurse(
                            savedAttachment,
                            (CardAction savedCardAction) =>
                        {
                            var savedData = savedCardAction.Value.ToJObject(true);

                            // This will not throw an exception if the saved action data is null
                            if (JToken.DeepEquals(savedData, incomingData))
                            {
                                result.Add(savedActivity, savedAttachment, savedCardAction);
                            }
                        },
                            TreeNodeType.Attachment,
                            TreeNodeType.CardAction);
                    }
                }
            }

            return(result);
        }
Example #3
0
        public async Task DeleteActionSourceAsync(ITurnContext turnContext, string dataIdScope, CancellationToken cancellationToken = default)
        {
            // TODO: Provide a way to delete elements by specifying an ID that's not in the incoming action data

            BotAssert.ContextNotNull(turnContext);

            if (string.IsNullOrEmpty(dataIdScope))
            {
                throw new ArgumentNullException(nameof(dataIdScope));
            }

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

            if (dataIdScope == DataIdScopes.Batch)
            {
                if (turnContext.GetIncomingActionData().ToJObject().GetIdFromActionData(DataIdScopes.Batch) is string batchId)
                {
                    var toDelete = new DataId(DataIdScopes.Batch, batchId);

                    // Iterate over a copy of the set so the original can be modified
                    foreach (var activity in state.SavedActivities.ToList())
                    {
                        // Delete any activity that contains the specified batch ID (data items are compared by value)
                        if (CardTree.GetIds(activity, TreeNodeType.Activity).Any(toDelete.Equals))
                        {
                            await DeleteActivityAsync(turnContext, activity, cancellationToken).ConfigureAwait(false);
                        }
                    }
                }
            }
            else
            {
                var matchResult = await GetDataMatchAsync(turnContext, cancellationToken).ConfigureAwait(false);

                var matchedActivity      = matchResult.SavedActivity;
                var matchedAttachment    = matchResult.SavedAttachment;
                var matchedAction        = matchResult.SavedAction;
                var shouldUpdateActivity = false;

                // TODO: Provide options for how to determine emptiness when cascading deletion
                // (e.g. when a card has no more actions rather than only when the card is completely empty)

                if (dataIdScope == DataIdScopes.Action && matchedActivity != null && matchedAttachment != null && matchedAction != null)
                {
                    if (matchedAttachment.ContentType.EqualsCI(ContentTypes.AdaptiveCard))
                    {
                        // For Adaptive Cards
                        if (matchedAction is JObject savedSubmitAction)
                        {
                            var adaptiveCard = (JObject)savedSubmitAction.Root;

                            // Remove the submit action from the Adaptive Card.
                            // SafeRemove will work whether the action is in an array
                            // or is the value of a select action property.
                            savedSubmitAction.SafeRemove();

                            matchedAttachment.Content = matchedAttachment.Content.FromJObject(adaptiveCard);
                            shouldUpdateActivity      = true;

                            // Check if the Adaptive Card is now empty
                            if (adaptiveCard.GetValue(AdaptiveProperties.Body).IsNullishOrEmpty() &&
                                adaptiveCard.GetValue(AdaptiveProperties.Actions).IsNullishOrEmpty())
                            {
                                // If the card is now empty, execute the next if block to delete it
                                dataIdScope = DataIdScopes.Card;
                            }
                        }
                    }
                    else
                    {
                        // For Bot Framework rich cards
                        if (matchedAction is CardAction cardAction)
                        {
                            // Remove the card action from the card
                            CardTree.Recurse(
                                matchedAttachment,
                                (IList <CardAction> actions) =>
                            {
                                actions.Remove(cardAction);
                            },
                                TreeNodeType.Attachment,
                                TreeNodeType.CardActionList);

                            shouldUpdateActivity = true;

                            // Check if the card is now empty.
                            // We are assuming that if a developer wants to make a rich card
                            // with only postBack/messageBack buttons then they will use a hero card
                            // and any other card would have more content than just postBack/messageBack buttons
                            // so only a hero card should potentially be empty at this point.
                            // We aren't checking if Buttons is null because it can't be at this point.
                            if (matchedAttachment.Content is HeroCard heroCard &&
                                !heroCard.Buttons.Any() &&
                                heroCard.Images?.Any() != true &&
                                string.IsNullOrWhiteSpace(heroCard.Title) &&
                                string.IsNullOrWhiteSpace(heroCard.Subtitle) &&
                                string.IsNullOrWhiteSpace(heroCard.Text))
                            {
                                // If the card is now empty, execute the next if block to delete it
                                dataIdScope = DataIdScopes.Card;
                            }
                        }
                    }
                }

                if (dataIdScope == DataIdScopes.Card && matchedActivity != null && matchedAttachment != null)
                {
                    matchedActivity.Attachments.Remove(matchedAttachment);

                    shouldUpdateActivity = true;

                    // Check if the activity is now empty
                    if (string.IsNullOrWhiteSpace(matchedActivity.Text) && !matchedActivity.Attachments.Any())
                    {
                        // If the activity is now empty, execute the next if block to delete it
                        dataIdScope = DataIdScopes.Carousel;
                    }
                }

                if (dataIdScope == DataIdScopes.Carousel && matchedActivity != null)
                {
                    await DeleteActivityAsync(turnContext, matchedActivity, cancellationToken).ConfigureAwait(false);
                }
                else if (shouldUpdateActivity)
                {
                    await UpdateActivityAsync(turnContext, matchedActivity, cancellationToken).ConfigureAwait(false);
                }
            }

            await StateAccessor.SetAsync(turnContext, state, cancellationToken).ConfigureAwait(false);
        }
        protected override async Task OnMessageActivityAsync(
            ITurnContext <IMessageActivity> turnContext,
            CancellationToken cancellationToken)
        {
            if (turnContext.GetIncomingActionData() is object incomingData)
            {
                var jObject = JObject.FromObject(incomingData);

                async Task SendPreservationFeedback()
                {
                    await turnContext.SendActivityAsync(
                        $"You sent the following data: {jObject[IdText]}, {jObject[IdNumber]}, {jObject[IdDate]}",
                        cancellationToken : cancellationToken);
                }

                switch (jObject["behavior"]?.ToString())
                {
                case BehaviorSubmit:

                    await SendPreservationFeedback();

                    break;

                case BehaviorPreserve:

                    await CardManager.PreserveValuesAsync(turnContext, cancellationToken);
                    await SendPreservationFeedback();

                    break;

                case BehaviorTranslate:

                    var language = jObject["language"]?.ToString();
                    var card     = CreateTranslationCard();

                    if (string.IsNullOrWhiteSpace(Translator.MicrosoftTranslatorConfig.SubscriptionKey))
                    {
                        if (string.IsNullOrWhiteSpace(language))
                        {
                            language = "Undefined";
                        }

                        Task <string> translateOne(string text, CancellationToken ct)
                        {
                            return(Task.FromResult($"{language}: {text}"));
                        }

                        card = await AdaptiveCardTranslator.TranslateAsync(card, translateOne, cancellationToken : cancellationToken);

                        await turnContext.SendActivityAsync("No subscription key was configured, so the card has been modified without a translation.");
                    }
                    else
                    {
                        if (string.IsNullOrWhiteSpace(language))
                        {
                            card = await Translator.TranslateAsync(card, cancellationToken);
                        }
                        else
                        {
                            card = await Translator.TranslateAsync(card, language, cancellationToken);
                        }
                    }

                    // There's no need to convert the card to a JObject
                    // since the library will do that for us
                    await turnContext.SendActivityAsync(MessageFactory.Attachment(new Attachment
                    {
                        ContentType = AdaptiveCard.ContentType,
                        Content     = card,
                    }), cancellationToken);

                    break;

                default:

                    if (jObject["label"] is JToken label)
                    {
                        await turnContext.SendActivityAsync(
                            $"Thank you for choosing {label}!",
                            cancellationToken : cancellationToken);
                    }

                    break;
                }
            }
            else
            {
                switch (turnContext.Activity.Text)
                {
                case DemoDeactivateActions:
                    await ShowDeactivationSample(turnContext, DataIdScopes.Action, cancellationToken);

                    break;

                case DemoDeactivateCards:
                    await ShowDeactivationSample(turnContext, DataIdScopes.Card, cancellationToken);

                    break;

                case DemoDeactivateCarousels:
                    await ShowDeactivationSample(turnContext, DataIdScopes.Carousel, cancellationToken);

                    break;

                case DemoDeactivateBatch:
                    await ShowDeactivationSample(turnContext, DataIdScopes.Batch, cancellationToken);

                    break;

                case DemoPreserveValues:
                    await ShowPreservationSample(turnContext, cancellationToken);

                    break;

                case DemoTranslateCards:
                    await ShowTranslationSample(turnContext, cancellationToken);

                    break;

                default:
                    await ShowMenu(turnContext, cancellationToken);

                    break;
                }
            }

            // The card manager will not work if its state is not saved
            await ConversationState.SaveChangesAsync(turnContext, cancellationToken : cancellationToken);
        }
        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);
            }
        }