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