public async Task DisableIdAsync( ITurnContext turnContext, DataId dataId, TrackingStyle style = TrackingStyle.TrackEnabled, CancellationToken cancellationToken = default) { BotAssert.ContextNotNull(turnContext); if (dataId is null) { throw new ArgumentNullException(nameof(dataId)); } switch (style) { case TrackingStyle.TrackEnabled: await ForgetIdAsync(turnContext, dataId, cancellationToken).ConfigureAwait(false); break; case TrackingStyle.TrackDisabled: await TrackIdAsync(turnContext, dataId, cancellationToken).ConfigureAwait(false); break; } }
// This will be called by the Bot Builder SDK and all three of these parameters are guaranteed to not be null private async Task <ResourceResponse[]> OnSendActivities(ITurnContext turnContext, List <Activity> activities, Func <Task <ResourceResponse[]> > next) { var options = GetOptionsForChannel(turnContext.Activity.ChannelId); // This method should be executed even if the bot is only sending text if (options.AutoClearEnabledOnSend && options.IdTrackingStyle == TrackingStyle.TrackEnabled && activities.Any(activity => activity.Type == ActivityTypes.Message)) { // TODO: Add an equivalent of this for channels with updates await Manager.ClearTrackedIdsAsync(turnContext).ConfigureAwait(false); } // The other methods will only be effective if the bot is sending attachments if (activities.Any(activity => activity.Attachments?.Any() != true)) { return(await next().ConfigureAwait(false)); } if (options.AutoConvertAdaptiveCards) { activities.ConvertAdaptiveCards(); } if (options.AutoSeparateAttachmentsOnSend) { activities.SeparateAttachments(); } if (options.AutoAdaptOutgoingCardActions) { activities.AdaptOutgoingCardActions(turnContext.Activity.ChannelId); } if (options.AutoApplyIds) { DataId.SetInBatch(activities, options.IdOptions); } // The resource response ID's will be automatically applied to the activities // so this return value is only passed along as the outer return value // and is not used for tracking/management. // The needed activity ID's can be extracted from the activities directly. var resourceResponses = await next().ConfigureAwait(false); if (options.AutoEnableOnSend && options.IdTrackingStyle == TrackingStyle.TrackEnabled) { foreach (var dataId in activities.GetIdsFromBatch()) { await Manager.EnableIdAsync(turnContext, dataId, options.IdTrackingStyle).ConfigureAwait(false); } } if (options.AutoSaveActivitiesOnSend) { await Manager.SaveActivitiesAsync(turnContext, activities).ConfigureAwait(false); } return(resourceResponses); }
// This will be called by the Bot Builder SDK and all three of these parameters are guaranteed to not be null private async Task <ResourceResponse> OnUpdateActivity(ITurnContext turnContext, Activity activity, Func <Task <ResourceResponse> > next) { var ignoreUpdate = turnContext.TurnState.Get <CardManagerTurnState>()?.MiddlewareIgnoreUpdate; // Removing the item also checks if it was present if (ignoreUpdate.Remove(activity)) { return(await next().ConfigureAwait(false)); } var options = GetOptionsForChannel(turnContext.Activity.ChannelId); var activities = new[] { activity }; // Some functionality that is in OnSendActivities is intentionally omitted here. // We don't clear enabled ID's because a new activity isn't being sent. // We don't separate attachments because it's impossible to update an activity with multiple activities. if (options.AutoConvertAdaptiveCards) { activities.ConvertAdaptiveCards(); } if (options.AutoAdaptOutgoingCardActions) { activities.AdaptOutgoingCardActions(turnContext.Activity.ChannelId); } if (options.AutoApplyIds) { DataId.SetInBatch(activities, options.IdOptions); } // The resource response ID will already be the ID of the activity // so this return value is only passed along as the outer return value // and is not used for tracking/management. // The needed activity ID can be extracted from the activity directly. var resourceResponse = await next().ConfigureAwait(false); if (options.AutoEnableOnSend && options.IdTrackingStyle == TrackingStyle.TrackEnabled) { foreach (var dataId in activities.GetIdsFromBatch()) { await Manager.EnableIdAsync(turnContext, dataId, options.IdTrackingStyle).ConfigureAwait(false); } } var state = await Manager.GetStateAsync(turnContext).ConfigureAwait(false); var savedActivity = state.SavedActivities.FirstOrDefault(a => a.Id == activity.Id); if (options.AutoSaveActivitiesOnSend || savedActivity != null) { await Manager.SaveActivitiesAsync(turnContext, activities).ConfigureAwait(false); } return(resourceResponse); }
public async Task TrackIdAsync(ITurnContext turnContext, DataId dataId, CancellationToken cancellationToken = default) { BotAssert.ContextNotNull(turnContext); if (dataId is null) { throw new ArgumentNullException(nameof(dataId)); } var state = await GetStateAsync(turnContext, cancellationToken).ConfigureAwait(false); state.DataIdsByScope.InitializeKey(dataId.Key, new HashSet <string>()).Add(dataId.Value); await StateAccessor.SetAsync(turnContext, state, cancellationToken).ConfigureAwait(false); }
public async Task ForgetIdAsync(ITurnContext turnContext, DataId dataId, CancellationToken cancellationToken = default) { BotAssert.ContextNotNull(turnContext); if (dataId is null) { throw new ArgumentNullException(nameof(dataId)); } var state = await GetStateAsync(turnContext, cancellationToken).ConfigureAwait(false); if (state.DataIdsByScope.TryGetValue(dataId.Key, out var ids)) { ids?.Remove(dataId.Value); } await StateAccessor.SetAsync(turnContext, state, cancellationToken).ConfigureAwait(false); }
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); }