// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System.Diagnostics.CodeAnalysis; using Microsoft.Teams.Apps.Schema.Entities; using Microsoft.Teams.Core.Schema; namespace Microsoft.Teams.Apps.Schema; /// /// Provides a fluent API for building TeamsActivity instances. /// public class TeamsActivityBuilder : CoreActivityBuilder { /// /// Initializes a new instance of the TeamsActivityBuilder class. /// internal TeamsActivityBuilder() : base(new TeamsActivity()) { } /// /// Initializes a new instance of the TeamsActivityBuilder class with an existing activity. /// /// The activity to build upon. internal TeamsActivityBuilder(TeamsActivity activity) : base(activity) { } /// /// Apply Conversation Reference from the specified activity. /// /// The source activity to copy conversation reference from. /// The builder instance for chaining. public TeamsActivityBuilder WithConversationReference(TeamsActivity activity) { ArgumentNullException.ThrowIfNull(activity); ArgumentNullException.ThrowIfNull(activity.ChannelId); ArgumentNullException.ThrowIfNull(activity.ServiceUrl); ArgumentNullException.ThrowIfNull(activity.Conversation); ArgumentNullException.ThrowIfNull(activity.From); ArgumentNullException.ThrowIfNull(activity.Recipient); WithServiceUrl(activity.ServiceUrl); WithChannelId(activity.ChannelId); WithConversation(activity.Conversation); WithFrom(activity.Recipient); return this; } /// /// Sets the sender account information. /// /// The sender account. /// The builder instance for chaining. public new TeamsActivityBuilder WithFrom(ConversationAccount? from) { _activity.From = from is TeamsConversationAccount teamsAccount ? teamsAccount : TeamsConversationAccount.FromConversationAccount(from)!; return this; } /// /// Sets the recipient account information. /// /// The recipient account. /// The builder instance for chaining. public new TeamsActivityBuilder WithRecipient(ConversationAccount? recipient) { _activity.Recipient = recipient is TeamsConversationAccount teamsAccount ? teamsAccount : TeamsConversationAccount.FromConversationAccount(recipient)!; return this; } /// /// Sets the recipient account information and optionally marks this as a targeted message. /// /// The recipient account. /// If true, marks this as a targeted message visible only to the specified recipient. /// The builder instance for chaining. [Experimental("ExperimentalTeamsTargeted")] public TeamsActivityBuilder WithRecipient(ConversationAccount? recipient, bool isTargeted) { if (recipient is not null) { recipient.IsTargeted = isTargeted ? true : null; _activity.Recipient = recipient is TeamsConversationAccount teamsAccount ? teamsAccount : TeamsConversationAccount.FromConversationAccount(recipient)!; } return this; } /// /// Sets the conversation information. /// /// The conversation information. /// The builder instance for chaining. public new TeamsActivityBuilder WithConversation(Conversation? conversation) { ArgumentNullException.ThrowIfNull(conversation); _activity.Conversation = conversation is TeamsConversation teamsConv ? teamsConv : TeamsConversation.FromConversation(conversation); return this; } /// /// Sets the Teams-specific channel data. /// /// The channel data. /// The builder instance for chaining. public TeamsActivityBuilder WithChannelData(TeamsChannelData? channelData) { _activity.ChannelData = channelData; return this; } /// /// Sets the entities collection. /// /// The entities collection. /// The builder instance for chaining. public TeamsActivityBuilder WithEntities(EntityList entities) { _activity.Entities = entities; return this; } /// /// Sets the attachments collection. /// /// The attachments collection. /// The builder instance for chaining. public TeamsActivityBuilder WithAttachments(IList attachments) { if (_activity is MessageActivity msg) msg.Attachments = attachments; else _activity.Properties["attachments"] = attachments; return this; } // TODO: Builders should only have "With" methods, not "Add" methods. /// /// Replaces the attachments collection with a single attachment. /// /// The attachment to set. Passing null clears the attachments. /// The builder instance for chaining. public TeamsActivityBuilder WithAttachment(TeamsAttachment? attachment) { IList? attachments = attachment is null ? null : [attachment]; if (_activity is MessageActivity msg) msg.Attachments = attachments; else _activity.Properties["attachments"] = attachments; return this; } /// /// Adds an entity to the activity's Entities collection. /// /// The entity to add. /// The builder instance for chaining. public TeamsActivityBuilder AddEntity(Entity entity) { _activity.Entities ??= []; _activity.Entities.Add(entity); return this; } /// /// Adds an attachment to the activity's Attachments collection. /// /// The attachment to add. /// The builder instance for chaining. public TeamsActivityBuilder AddAttachment(TeamsAttachment attachment) { if (_activity is MessageActivity msg) { msg.Attachments ??= []; msg.Attachments.Add(attachment); } else { if (!_activity.Properties.TryGetValue("attachments", out object? existing) || existing is not List list) { list = []; _activity.Properties["attachments"] = list; } list.Add(attachment); } return this; } /// /// Adds an Adaptive Card attachment to the activity. /// /// The Adaptive Card payload. /// Optional callback to further configure the attachment before it is added. /// The builder instance for chaining. public TeamsActivityBuilder AddAdaptiveCardAttachment(object adaptiveCard, Action? configure = null) { TeamsAttachment attachment = BuildAdaptiveCardAttachment(adaptiveCard, configure); return AddAttachment(attachment); } /// /// Sets the activity attachments collection to a single Adaptive Card attachment. /// /// The Adaptive Card payload. /// Optional callback to further configure the attachment. /// The builder instance for chaining. public TeamsActivityBuilder WithAdaptiveCardAttachment(object adaptiveCard, Action? configure = null) { TeamsAttachment attachment = BuildAdaptiveCardAttachment(adaptiveCard, configure); return WithAttachment(attachment); } /// /// Adds or sets the text content of the activity. /// /// /// /// public TeamsActivityBuilder WithText(string text, string textFormat = "plain") { if (_activity is MessageActivity msg) { msg.Text = text; msg.TextFormat = textFormat; } else { _activity.Properties["text"] = text; _activity.Properties["textFormat"] = textFormat; } return this; } /// /// With Suggested Actions /// /// /// public TeamsActivityBuilder WithSuggestedActions(SuggestedActions suggestedActions) { ArgumentNullException.ThrowIfNull(_activity); _activity.SuggestedActions = suggestedActions; return this; } /// /// Adds a quoted reply entity and appends a placeholder to the activity text. /// The activity type must be set to Message (via ) before calling this method. /// /// The ID of the message to quote. /// Optional text, appended to the quoted message placeholder. /// The builder instance for chaining. /// Thrown when the activity type is not Message. [Experimental("ExperimentalTeamsQuotedReplies")] public TeamsActivityBuilder AddQuote(string messageId, string? text = null) { ArgumentException.ThrowIfNullOrWhiteSpace(messageId); if (_activity.Type != TeamsActivityType.Message) { throw new InvalidOperationException("AddQuote can only be used on message activities. Call WithType(TeamsActivityType.Message) first."); } QuotedReplyEntityExtensions.AddToActivity(_activity, messageId, text); return this; } /// /// Adds a targetedMessageInfo entity for Prompt Preview, referencing the inbound targeted-message id. /// Any existing quotedReply entities and matching <quoted messageId="..."/> placeholders are stripped /// to prevent collision with prompt preview. If a targetedMessageInfo entity is already present, no new entity is added. /// The activity type must be set to Message (via ) before calling this method. /// /// /// After the placeholder strip, the activity text is trimmed of leading and trailing whitespace. /// /// The id of the inbound targeted message being responded to. /// The builder instance for chaining. /// Thrown when the activity type is not Message. [Experimental("ExperimentalTeamsTargeted")] public TeamsActivityBuilder WithTargetedMessageInfo(string messageId) { ArgumentException.ThrowIfNullOrWhiteSpace(messageId); if (_activity.Type != TeamsActivityType.Message) { throw new InvalidOperationException("WithTargetedMessageInfo can only be used on message activities. Call WithType(TeamsActivityType.Message) first."); } TargetedMessageInfoEntityExtensions.AddToActivity(_activity, messageId); return this; } /// /// Adds a mention to the activity. /// /// The account to mention. /// Optional custom text for the mention. If null, uses the account name. /// Whether to prepend the mention text to the activity's text content. /// The builder instance for chaining. public TeamsActivityBuilder AddMention(ConversationAccount account, string? text = null, bool addText = true) { ArgumentNullException.ThrowIfNull(account); MentionEntityExtensions.AddToActivity(_activity, account, text, addText); return this; } /// /// Adds a clientInfo entity to the activity. /// /// The client platform (for example, Web or Desktop). /// The client's country/region code. /// The client's IANA timezone. /// The client's locale (for example, en-US). /// The builder instance for chaining. public TeamsActivityBuilder AddClientInfo(string? platform, string? country, string? timezone, string? locale) { ClientInfoEntityExtensions.AddToActivity(_activity, platform, country, timezone, locale); return this; } /// /// Adds a productInfo entity to the activity. /// /// The product identifier. /// The builder instance for chaining. public TeamsActivityBuilder AddProductInfo(string? id) { ProductInfoEntityExtensions.AddToActivity(_activity, id); return this; } /// /// Adds the AI-generated content label to the root message entity. /// public TeamsActivityBuilder AddAIGenerated() { ArgumentNullException.ThrowIfNull(_activity); OMessageEntityExtensions.AddAIGeneratedContent(_activity); return this; } /// /// Enables/disables feedback loop on the activity. /// public TeamsActivityBuilder AddFeedback(bool value = true) { ArgumentNullException.ThrowIfNull(_activity); _activity.ChannelData ??= new TeamsChannelData(); _activity.ChannelData.FeedbackLoopEnabled = value; return this; } /// /// Configures feedback loop mode on the activity. /// /// The feedback loop type. See for known values. /// The builder instance for chaining. public TeamsActivityBuilder AddFeedback(string mode) { ArgumentNullException.ThrowIfNull(_activity); _activity.ChannelData ??= new TeamsChannelData(); _activity.ChannelData.FeedbackLoop = new FeedbackLoop(mode); _activity.ChannelData.FeedbackLoopEnabled = null; return this; } /// /// Adds a citation claim to the activity. /// public TeamsActivityBuilder AddCitation(int position, CitationAppearance appearance) { ArgumentNullException.ThrowIfNull(_activity); ArgumentNullException.ThrowIfNull(appearance); _activity.Entities ??= []; CitationEntityExtensions.AddToActivity(_activity, position, appearance); return this; } /// /// Adds a content sensitivity label to the activity. /// /// The name of the sensitivity label. /// Optional description of the sensitivity label. /// Optional pattern associated with the sensitivity label. /// The builder instance for chaining. public TeamsActivityBuilder AddSensitivityLabel(string name, string? description = null, DefinedTerm? pattern = null) { ArgumentNullException.ThrowIfNull(_activity); SensitiveUsageEntityExtensions.AddToActivity(_activity, name, description, pattern); return this; } /// /// Builds and returns the configured TeamsActivity instance. /// /// The configured TeamsActivity. public override TeamsActivity Build() { return _activity; } private static TeamsAttachment BuildAdaptiveCardAttachment(object adaptiveCard, Action? configure) { ArgumentNullException.ThrowIfNull(adaptiveCard); TeamsAttachmentBuilder attachmentBuilder = TeamsAttachment .CreateBuilder() .WithAdaptiveCard(adaptiveCard); configure?.Invoke(attachmentBuilder); return attachmentBuilder.Build(); } }