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