diff --git a/src/libraries/Core/Microsoft.Agents.Core/Models/Activity.cs b/src/libraries/Core/Microsoft.Agents.Core/Models/Activity.cs index 6fcac7ef0..c7f17fc6e 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/Models/Activity.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/Models/Activity.cs @@ -14,7 +14,7 @@ namespace Microsoft.Agents.Core.Models { /// - public class Activity : + public partial class Activity : IActivity, IConversationUpdateActivity, IContactRelationUpdateActivity, @@ -142,6 +142,268 @@ public bool IsType(string type) return string.Equals(type, Type, StringComparison.OrdinalIgnoreCase); } + /// + public IActivity WithText(string text) + { + Text = text; + return this; + } + + /// + public IActivity WithSpeak(string speak) + { + Speak = speak; + return this; + } + + /// + public IActivity WithInputHint(string inputHint) + { + InputHint = inputHint; + return this; + } + + /// + public IActivity WithSummary(string summary) + { + Summary = summary; + return this; + } + + /// + public IActivity WithLocale(string locale) + { + Locale = locale; + return this; + } + + /// + public IActivity WithTextFormat(string textFormat) + { + TextFormat = textFormat; + return this; + } + + /// + public IActivity WithAttachmentLayout(string attachmentLayout) + { + AttachmentLayout = attachmentLayout; + return this; + } + + /// + public IActivity WithDeliveryMode(string deliveryMode) + { + DeliveryMode = deliveryMode; + return this; + } + + /// + public IActivity WithName(string name) + { + Name = name; + return this; + } + + /// + public IActivity WithValue(object value) + { + Value = value; + return this; + } + + /// + public IActivity WithValue(object value, string valueType) + { + Value = value; + ValueType = valueType; + return this; + } + + /// + public IActivity WithSuggestedActions(SuggestedActions suggestedActions) + { + SuggestedActions = suggestedActions; + return this; + } + + /// + public IActivity AddText(string text) + { + Text += text; + return this; + } + + /// + public IActivity AddAttachment(params Attachment[] attachments) + { + if (attachments == null) + { + return this; + } + + Attachments ??= []; + foreach (var attachment in attachments) + { + Attachments.Add(attachment); + } + + return this; + } + + /// + public IActivity AddCard(Card card) => card == null ? this : AddAttachment(card.ToAttachment()); + + /// + public IActivity AddEntity(params Entity[] entities) + { + if (entities == null) + { + return this; + } + + Entities ??= []; + foreach (var entity in entities) + { + Entities.Add(entity); + } + + return this; + } + + /// + public bool IsMessage() => IsType(ActivityTypes.Message); + + /// + public bool IsEvent() => IsType(ActivityTypes.Event); + + /// + public bool IsInvoke() => IsType(ActivityTypes.Invoke); + + /// + public bool IsTyping() => IsType(ActivityTypes.Typing); + + /// + public bool IsConversationUpdate() => IsType(ActivityTypes.ConversationUpdate); + + /// + public bool IsEndOfConversation() => IsType(ActivityTypes.EndOfConversation); + + /// + public bool IsHandoff() => IsType(ActivityTypes.Handoff); + + /// + public bool IsTrace() => IsType(ActivityTypes.Trace); + + /// + public bool IsCommand() => IsType(ActivityTypes.Command); + + /// + public bool IsCommandResult() => IsType(ActivityTypes.CommandResult); + + /// + public IActivity AddMention(ChannelAccount account, string text = null, bool addText = true) + { + var mentionText = text ?? account?.Name; + var markup = $"{mentionText}"; + + if (addText) + { + Text = string.IsNullOrEmpty(Text) ? markup : $"{markup} {Text}"; + } + + return AddEntity(new Mention(mentioned: account, text: markup)); + } + + /// + public Mention[] GetMentions() + { + var result = new List(); + if (Entities != null) + { + foreach (var entity in Entities) + { + if (entity is Mention mention) + { + result.Add(mention); + } + } + } + + return [.. result]; + } + + /// + public Mention GetAccountMention(string accountId) + { + if (Entities == null || accountId == null) + { + return null; + } + + foreach (var entity in Entities) + { + if (entity is Mention mention && mention.Mentioned?.Id == accountId) + { + return mention; + } + } + + return null; + } + + /// + public bool IsRecipientMentioned() + { + return Recipient?.Id != null && GetAccountMention(Recipient.Id) != null; + } + + /// + public string RemoveRecipientMention() + { + return this.RemoveMentionText(Recipient?.Id); + } + + /// + public bool IsTargetedActivity() + { + if (Entities == null || Entities.Count == 0) + { + return false; + } + + foreach (var entity in Entities) + { + if (entity.Type == EntityTypes.ActivityTreatment && entity is ActivityTreatment treatment && treatment.Treatment == ActivityTreatmentTypes.Targeted) + { + return true; + } + } + + return false; + } + + /// + public IActivity MakeTargetedActivity(ChannelAccount user = null) + { + if (IsTargetedActivity()) + { + return this; + } + + if (Recipient == null && user == null) + { + throw new InvalidOperationException("Cannot mark activity as targeted because both the Activity.Recipient and `user` argument are null. At least one must be provided."); + } + + Entities ??= []; + Entities.Add(new ActivityTreatment() { Treatment = ActivityTreatmentTypes.Targeted }); + + Recipient = user ?? Recipient; + + return this; + } + /// public string Type { get; set; } diff --git a/src/libraries/Core/Microsoft.Agents.Core/Models/AdaptiveCardCard.cs b/src/libraries/Core/Microsoft.Agents.Core/Models/AdaptiveCardCard.cs new file mode 100644 index 000000000..5ff08fbed --- /dev/null +++ b/src/libraries/Core/Microsoft.Agents.Core/Models/AdaptiveCardCard.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable disable + +using System.IO; +using System.Text; + +namespace Microsoft.Agents.Core.Models +{ + /// A card that carries a raw Adaptive Card JSON payload. + /// + /// The JSON is stored verbatim in and unpacked into the attachment as nested + /// JSON when the activity is serialized. + /// + public class AdaptiveCardCard : Card + { + /// + /// The content type value of an . + /// + public const string ContentType = Models.ContentTypes.AdaptiveCard; + + /// The Adaptive Card content as a JSON string. + public string Content; + + /// Initializes a new instance of from an Adaptive Card JSON string. + /// The Adaptive Card content as a JSON string. + public AdaptiveCardCard(string json) + { + Content = json; + } + + /// Initializes a new instance of from a stream containing Adaptive Card JSON. + /// A stream containing the Adaptive Card JSON. The stream is left open. + public AdaptiveCardCard(Stream stream) + { + using var reader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true); + Content = reader.ReadToEnd(); + } + + /// + public override Attachment ToAttachment() + { + return new Attachment + { + ContentType = ContentType, + Content = Content + }; + } + } +} diff --git a/src/libraries/Core/Microsoft.Agents.Core/Models/AnimationCard.cs b/src/libraries/Core/Microsoft.Agents.Core/Models/AnimationCard.cs index 9ebf7f8e4..76fbe8b43 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/Models/AnimationCard.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/Models/AnimationCard.cs @@ -9,7 +9,7 @@ namespace Microsoft.Agents.Core.Models { /// An animation card (Ex: gif or short video clip). - public class AnimationCard + public class AnimationCard : Card { /// Initializes a new instance of AnimationCard. public AnimationCard() @@ -57,7 +57,7 @@ public AnimationCard(string title = default, string subtitle = default, string t /// /// The instance of . /// The generated attachment. - public Attachment ToAttachment() + public override Attachment ToAttachment() { return new Attachment { @@ -66,22 +66,6 @@ public Attachment ToAttachment() }; } - /// - /// Creates a new message activity that includes this animation card as an attachment. - /// - /// Use this method to generate a message activity suitable for sending to a user, with - /// the current animation card included as an attachment. The returned activity has its type set to . - /// An representing a message activity with the animation card attached. - public IActivity ToMessage() - { - return new Activity - { - Type = ActivityTypes.Message, - Attachments = [ToAttachment()] - }; - } - /// Title of this card. public string Title { get; set; } /// Subtitle of this card. @@ -107,5 +91,12 @@ public IActivity ToMessage() /// Supplementary parameter for this card. [JsonConverter(typeof(Serialization.Converters.ObjectTypeConverter))] public object Value { get; set; } + + /// Adds a media URL and returns this card. + public AnimationCard AddMedia(MediaUrl media) { Media ??= []; Media.Add(media); return this; } + /// Adds a media URL by string and returns this card. + public AnimationCard AddMedia(string url, string profile = null) { Media ??= []; Media.Add(new MediaUrl(url, profile)); return this; } + /// Adds a button and returns this card. + public AnimationCard AddButton(CardAction button) { Buttons ??= []; Buttons.Add(button); return this; } } } diff --git a/src/libraries/Core/Microsoft.Agents.Core/Models/AudioCard.cs b/src/libraries/Core/Microsoft.Agents.Core/Models/AudioCard.cs index b758fff7b..fba653a46 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/Models/AudioCard.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/Models/AudioCard.cs @@ -9,7 +9,7 @@ namespace Microsoft.Agents.Core.Models { /// Audio card. - public class AudioCard + public class AudioCard : Card { /// Initializes a new instance of AudioCard. public AudioCard() @@ -57,7 +57,7 @@ public AudioCard(string title = default, string subtitle = default, string text /// /// The instance of . /// The generated attachment. - public Attachment ToAttachment() + public override Attachment ToAttachment() { return new Attachment { @@ -66,22 +66,6 @@ public Attachment ToAttachment() }; } - /// - /// Creates a new message activity that includes this audio card as an attachment. - /// - /// Use this method to generate a message activity suitable for sending to a user, with - /// the current audio card included as an attachment. The returned activity has its type set to . - /// An representing a message activity with the audio card attached. - public IActivity ToMessage() - { - return new Activity - { - Type = ActivityTypes.Message, - Attachments = [ToAttachment()] - }; - } - /// Title of this card. public string Title { get; set; } /// Subtitle of this card. @@ -107,5 +91,12 @@ public IActivity ToMessage() /// Supplementary parameter for this card. [JsonConverter(typeof(Serialization.Converters.ObjectTypeConverter))] public object Value { get; set; } + + /// Adds a media URL and returns this card. + public AudioCard AddMedia(MediaUrl media) { Media ??= []; Media.Add(media); return this; } + /// Adds a media URL by string and returns this card. + public AudioCard AddMedia(string url, string profile = null) { Media ??= []; Media.Add(new MediaUrl(url, profile)); return this; } + /// Adds a button and returns this card. + public AudioCard AddButton(CardAction button) { Buttons ??= []; Buttons.Add(button); return this; } } } diff --git a/src/libraries/Core/Microsoft.Agents.Core/Models/BasicCard.cs b/src/libraries/Core/Microsoft.Agents.Core/Models/BasicCard.cs index 4aa217229..7c6f4ebcd 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/Models/BasicCard.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/Models/BasicCard.cs @@ -3,11 +3,13 @@ #nullable disable +using System; using System.Collections.Generic; namespace Microsoft.Agents.Core.Models { /// A basic card. + [Obsolete("BasicCard is not an Activity Protocol card type (it has no content type) and will be removed in a future release.")] public class BasicCard { /// Initializes a new instance of BasicCard. @@ -46,5 +48,16 @@ public BasicCard(string title = default, string subtitle = default, string text public IList Buttons { get; set; } /// A clickable action. public CardAction Tap { get; set; } + + /// Adds an image and returns this card. + public BasicCard AddImage(CardImage image) { Images ??= []; Images.Add(image); return this; } + /// Adds an image by URL and returns this card. + public BasicCard AddImage(string url, string alt = null) { Images ??= []; Images.Add(new CardImage(url, alt)); return this; } + /// Adds a button and returns this card. + public BasicCard AddButton(CardAction button) { Buttons ??= []; Buttons.Add(button); return this; } + /// Adds a button by title/type/value and returns this card. + public BasicCard AddButton(string title, string type = ActionTypes.ImBack, object value = null) { Buttons ??= []; Buttons.Add(new CardAction(type: type, title: title, value: value ?? title)); return this; } + /// Adds one or more buttons and returns this card. + public BasicCard AddButtons(params CardAction[] buttons) { if (buttons == null) { return this; } Buttons ??= []; foreach (var button in buttons) { Buttons.Add(button); } return this; } } } diff --git a/src/libraries/Core/Microsoft.Agents.Core/Models/Card.cs b/src/libraries/Core/Microsoft.Agents.Core/Models/Card.cs new file mode 100644 index 000000000..9484e0d4c --- /dev/null +++ b/src/libraries/Core/Microsoft.Agents.Core/Models/Card.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable disable + +namespace Microsoft.Agents.Core.Models +{ + /// + /// Base class for rich cards that can be sent to a user as an . + /// + /// + /// Each concrete card implements to wrap itself in an attachment using its own + /// content type. builds on that to produce a ready-to-send message activity. + /// + public abstract class Card + { + /// + /// Creates a new that wraps this card. + /// + /// The generated attachment. + public abstract Attachment ToAttachment(); + + /// + /// Creates a new message activity that includes this card as an attachment. + /// + /// Use this method to generate a message activity suitable for sending to a user, with + /// the current card included as an attachment. The returned activity has its type set to . + /// An representing a message activity with the card attached. + public virtual IActivity ToMessage() + { + return new Activity + { + Type = ActivityTypes.Message, + Attachments = [ToAttachment()] + }; + } + } +} diff --git a/src/libraries/Core/Microsoft.Agents.Core/Models/HeroCard.cs b/src/libraries/Core/Microsoft.Agents.Core/Models/HeroCard.cs index 81a692038..3752daf5e 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/Models/HeroCard.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/Models/HeroCard.cs @@ -8,7 +8,7 @@ namespace Microsoft.Agents.Core.Models { /// A Hero card (card with a single, large image). - public class HeroCard + public class HeroCard : Card { /// Initializes a new instance of HeroCard. public HeroCard() @@ -44,7 +44,7 @@ public HeroCard(string title = default, string subtitle = default, string text = /// /// The instance of . /// The generated attachment. - public Attachment ToAttachment() + public override Attachment ToAttachment() { return new Attachment { @@ -53,22 +53,6 @@ public Attachment ToAttachment() }; } - /// - /// Creates a new message activity that includes this hero card as an attachment. - /// - /// Use this method to generate a message activity suitable for sending to a user, with - /// the current hero card included as an attachment. The returned activity has its type set to . - /// An representing a message activity with the hero card attached. - public IActivity ToMessage() - { - return new Activity - { - Type = ActivityTypes.Message, - Attachments = [ToAttachment()] - }; - } - /// Title of the card. public string Title { get; set; } /// Subtitle of the card. @@ -81,5 +65,16 @@ public IActivity ToMessage() public IList Buttons { get; set; } /// A clickable action. public CardAction Tap { get; set; } + + /// Adds an image and returns this card. + public HeroCard AddImage(CardImage image) { Images ??= []; Images.Add(image); return this; } + /// Adds an image by URL and returns this card. + public HeroCard AddImage(string url, string alt = null) { Images ??= []; Images.Add(new CardImage(url, alt)); return this; } + /// Adds a button and returns this card. + public HeroCard AddButton(CardAction button) { Buttons ??= []; Buttons.Add(button); return this; } + /// Adds a button by title/type/value and returns this card. + public HeroCard AddButton(string title, string type = ActionTypes.ImBack, object value = null) { Buttons ??= []; Buttons.Add(new CardAction(type: type, title: title, value: value ?? title)); return this; } + /// Adds one or more buttons and returns this card. + public HeroCard AddButtons(params CardAction[] buttons) { if (buttons == null) { return this; } Buttons ??= []; foreach (var button in buttons) { Buttons.Add(button); } return this; } } } diff --git a/src/libraries/Core/Microsoft.Agents.Core/Models/IActivity.cs b/src/libraries/Core/Microsoft.Agents.Core/Models/IActivity.cs index 7bd5d9e06..65a42ac92 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/Models/IActivity.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/Models/IActivity.cs @@ -270,6 +270,8 @@ public interface IActivity /// /// The type field controls the meaning of each Activity, and are by convention short strings (e.g. "message"). + /// In most cases you should assign one of the well-known values from + /// (e.g. ) rather than a raw string literal. /// Senders may define their own application-layer types, although they are encouraged to choose values that are /// unlikely to collide with future well-defined values. If senders use URIs as type values, they SHOULD NOT /// implement URI ladder comparisons to establish equivalence. @@ -336,6 +338,137 @@ public interface IActivity /// The new trace Activity. IActivity CreateTrace(string name, object value = null, string valueType = null, [CallerMemberName] string label = null); + /// Sets and returns this activity. + IActivity WithText(string text); + + /// Sets and returns this activity. + IActivity WithSpeak(string speak); + + /// Sets and returns this activity. + IActivity WithInputHint(string inputHint); + + /// Sets and returns this activity. + IActivity WithSummary(string summary); + + /// Sets and returns this activity. + IActivity WithLocale(string locale); + + /// Sets and returns this activity. + IActivity WithTextFormat(string textFormat); + + /// Sets and returns this activity. + IActivity WithAttachmentLayout(string attachmentLayout); + + /// Sets and returns this activity. + IActivity WithDeliveryMode(string deliveryMode); + + /// Sets and returns this activity. + IActivity WithName(string name); + + /// Sets and returns this activity. + IActivity WithValue(object value); + + /// Sets both and and returns this activity. + IActivity WithValue(object value, string valueType); + + /// Sets and returns this activity. + IActivity WithSuggestedActions(SuggestedActions suggestedActions); + + /// Appends the given text to and returns this activity. + IActivity AddText(string text); + + /// Adds one or more attachments to and returns this activity. + IActivity AddAttachment(params Attachment[] attachments); + + /// Adds a as an attachment and returns this activity. + IActivity AddCard(Card card); + + /// Adds one or more entities to and returns this activity. + IActivity AddEntity(params Entity[] entities); + + /// + /// Adds a entity for the given account and, optionally, + /// prepends the mention markup to . + /// + /// The account being mentioned. + /// Optional display text for the mention. Defaults to . + /// When true (default), the <at> mention markup is prepended to the activity text. + /// This activity. + IActivity AddMention(ChannelAccount account, string text = null, bool addText = true); + + /// + /// Gets the for a specific account, if this activity mentions it. + /// + /// The to look for. + /// The matching ; or null if the account is not mentioned. + Mention GetAccountMention(string accountId); + + /// + /// Indicates whether the activity's is mentioned in the activity. + /// + /// true if the recipient is mentioned; otherwise, false. + bool IsRecipientMentioned(); + + /// Returns true when the activity type is . + bool IsMessage(); + + /// Returns true when the activity type is . + bool IsEvent(); + + /// Returns true when the activity type is . + bool IsInvoke(); + + /// Returns true when the activity type is . + bool IsTyping(); + + /// Returns true when the activity type is . + bool IsConversationUpdate(); + + /// Returns true when the activity type is . + bool IsEndOfConversation(); + + /// Returns true when the activity type is . + bool IsHandoff(); + + /// Returns true when the activity type is . + bool IsTrace(); + + /// Returns true when the activity type is . + bool IsCommand(); + + /// Returns true when the activity type is . + bool IsCommandResult(); + + /// + /// Resolves the mentions from the entities of this activity. + /// + /// The array of mentions; or an empty array, if none are found. + /// Intended for use with a message activity, where the activity is + /// . + /// + Mention[] GetMentions(); + + /// + /// Determines whether this activity represents a targeted activity treatment. + /// + /// true if the activity contains an entity with a + /// treatment of Targeted; otherwise, false. + bool IsTargetedActivity(); + + /// + /// Marks this activity as targeted by adding a targeted treatment entity to its collection. + /// + /// Optional user to set as the recipient. Replaces when provided. + /// This activity. + IActivity MakeTargetedActivity(ChannelAccount user = null); + + /// + /// Removes the recipient mention text from the property. + /// + /// Use with caution because this function alters the on the activity. + /// The new value. + string RemoveRecipientMention(); + /// /// Gets properties that are not otherwise defined by the type but that /// might appear in the serialized REST JSON object. diff --git a/src/libraries/Core/Microsoft.Agents.Core/Models/IActivityExtensions.cs b/src/libraries/Core/Microsoft.Agents.Core/Models/IActivityExtensions.cs index 9892fa031..1bd62c883 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/Models/IActivityExtensions.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/Models/IActivityExtensions.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; -using System.Collections.Generic; using System.Text.Json; using Microsoft.Agents.Core.Serialization; @@ -23,40 +21,6 @@ public static string ToJson(this IActivity activity) return JsonSerializer.Serialize(activity, ProtocolJsonSerializer.SerializationOptions); } - /// - /// Resolves the mentions from the entities of this activity. - /// - /// The array of mentions; or an empty array, if none are found. - /// This method is defined on the class, but is only intended - /// for use with a message activity, where the activity is set to - /// . - /// - public static Mention[] GetMentions(this IActivity activity) - { - var result = new List(); - if (activity.Entities != null) - { - foreach (var entity in activity.Entities) - { - if (entity is Mention mention) - { - result.Add(mention); - } - } - } - return [.. result]; - } - - /// - /// Remove recipient mention text from Text property. - /// Use with caution because this function is altering the text on the Activity. - /// - /// new .Text property value. - public static string RemoveRecipientMention(this T activity) where T : IActivity - { - return activity.RemoveMentionText(activity.Recipient?.Id); - } - /// /// Clone the activity to a new instance of activity. /// @@ -118,62 +82,5 @@ public static bool TryGetChannelData(this IActivity activity, out T instance) return false; } } - - /// - /// Determines whether the specified activity represents a targeted activity treatment. - /// - /// Use this method to identify activities that are specifically marked as targeted - /// treatments. This can be useful for filtering or processing activities based on their treatment - /// type. - /// The activity to evaluate for targeted treatment. Cannot be null. - /// true if the activity contains an entity of type ActivityTreatment with a treatment of Targeted; otherwise, - /// false. - public static bool IsTargetedActivity(this IActivity activity) - { - if (activity.Entities == null || activity.Entities.Count == 0) - { - return false; - } - - foreach (var entity in activity.Entities) - { - if (entity.Type == EntityTypes.ActivityTreatment && entity is ActivityTreatment treatment && treatment.Treatment == ActivityTreatmentTypes.Targeted) - { - return true; - } - } - - return false; - } - - /// - /// Marks the specified activity as targeted by adding a targeted treatment entity to its collection. - /// - /// This method ensures that the activity's Entities collection is initialized before - /// adding the targeted treatment. Use this extension to indicate that an activity should be processed or - /// interpreted as targeted in downstream workflows. - /// The activity to be marked as targeted. Cannot be null. - /// The user to be set as the recipient. This replaces Activity.Recipient if provided. - public static IActivity MakeTargetedActivity(this IActivity activity, ChannelAccount user = null) - { - AssertionHelpers.ThrowIfNull(activity, nameof(activity)); - - if (activity.IsTargetedActivity()) - { - return activity; - } - - if (activity.Recipient == null && user == null) - { - throw new InvalidOperationException("Cannot mark activity as targeted because both the Activity.Recipient and `user` argument are null. At least one must be provided."); - } - - activity.Entities ??= []; - activity.Entities.Add(new ActivityTreatment() { Treatment = ActivityTreatmentTypes.Targeted }); - - activity.Recipient = user ?? activity.Recipient; - - return activity; - } } } \ No newline at end of file diff --git a/src/libraries/Core/Microsoft.Agents.Core/Models/MediaCard.cs b/src/libraries/Core/Microsoft.Agents.Core/Models/MediaCard.cs index be892a4a9..99def419a 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/Models/MediaCard.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/Models/MediaCard.cs @@ -3,12 +3,14 @@ #nullable disable +using System; using System.Collections.Generic; using System.Text.Json.Serialization; namespace Microsoft.Agents.Core.Models { /// Media card. + [Obsolete("MediaCard is a structural base without an Activity Protocol content type. Use AnimationCard, AudioCard, or VideoCard instead. Will be removed in a future release.")] public class MediaCard { /// Initializes a new instance of MediaCard. @@ -72,5 +74,12 @@ public MediaCard(string title = default, string subtitle = default, string text /// Supplementary parameter for this card. [JsonConverter(typeof(Serialization.Converters.ObjectTypeConverter))] public object Value { get; set; } + + /// Adds a media URL and returns this card. + public MediaCard AddMedia(MediaUrl media) { Media ??= []; Media.Add(media); return this; } + /// Adds a media URL by string and returns this card. + public MediaCard AddMedia(string url, string profile = null) { Media ??= []; Media.Add(new MediaUrl(url, profile)); return this; } + /// Adds a button and returns this card. + public MediaCard AddButton(CardAction button) { Buttons ??= []; Buttons.Add(button); return this; } } } diff --git a/src/libraries/Core/Microsoft.Agents.Core/Models/OAuthCard.cs b/src/libraries/Core/Microsoft.Agents.Core/Models/OAuthCard.cs index b7292c5d0..8296af10d 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/Models/OAuthCard.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/Models/OAuthCard.cs @@ -8,7 +8,7 @@ namespace Microsoft.Agents.Core.Models { /// A card representing a request to perform a sign in via OAuth. - public class OAuthCard + public class OAuthCard : Card { /// Initializes a new instance of OAuthCard. public OAuthCard() @@ -37,7 +37,7 @@ public OAuthCard(string text = default, string connectionName = default, IList /// The instance of . /// The generated attachment. - public Attachment ToAttachment() + public override Attachment ToAttachment() { return new Attachment { diff --git a/src/libraries/Core/Microsoft.Agents.Core/Models/ReceiptCard.cs b/src/libraries/Core/Microsoft.Agents.Core/Models/ReceiptCard.cs index 43e48e83b..7e41b6c80 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/Models/ReceiptCard.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/Models/ReceiptCard.cs @@ -8,7 +8,7 @@ namespace Microsoft.Agents.Core.Models { /// A receipt card. - public class ReceiptCard + public class ReceiptCard : Card { /// Initializes a new instance of ReceiptCard. public ReceiptCard() @@ -49,7 +49,7 @@ public ReceiptCard(string title = default, IList facts = default, IList /// The instance of . /// The generated attachment. - public Attachment ToAttachment() + public override Attachment ToAttachment() { return new Attachment { @@ -58,22 +58,6 @@ public Attachment ToAttachment() }; } - /// - /// Creates a new message activity that includes this receipt card as an attachment. - /// - /// Use this method to generate a message activity suitable for sending to a user, with - /// the current receipt card included as an attachment. The returned activity has its type set to . - /// An representing a message activity with the receipt card attached. - public IActivity ToMessage() - { - return new Activity - { - Type = ActivityTypes.Message, - Attachments = [ToAttachment()] - }; - } - /// Title of the card. public string Title { get; set; } @@ -91,5 +75,12 @@ public IActivity ToMessage() public string Vat { get; set; } /// Set of actions applicable to the current card. public IList Buttons { get; set; } + + /// Adds a fact and returns this card. + public ReceiptCard AddFact(Fact fact) { Facts ??= []; Facts.Add(fact); return this; } + /// Adds a receipt item and returns this card. + public ReceiptCard AddItem(ReceiptItem item) { Items ??= []; Items.Add(item); return this; } + /// Adds a button and returns this card. + public ReceiptCard AddButton(CardAction button) { Buttons ??= []; Buttons.Add(button); return this; } } } diff --git a/src/libraries/Core/Microsoft.Agents.Core/Models/SigninCard.cs b/src/libraries/Core/Microsoft.Agents.Core/Models/SigninCard.cs index 568713102..5a4814b2c 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/Models/SigninCard.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/Models/SigninCard.cs @@ -8,7 +8,7 @@ namespace Microsoft.Agents.Core.Models { /// A card representing a request to sign in. - public class SigninCard + public class SigninCard : Card { public SigninCard() { @@ -34,7 +34,7 @@ public SigninCard(string text = default, IList buttons = default) /// /// The instance of . /// The generated attachment. - public Attachment ToAttachment() + public override Attachment ToAttachment() { return new Attachment { diff --git a/src/libraries/Core/Microsoft.Agents.Core/Models/SuggestedActions.cs b/src/libraries/Core/Microsoft.Agents.Core/Models/SuggestedActions.cs index 943e6f2fb..901b77778 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/Models/SuggestedActions.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/Models/SuggestedActions.cs @@ -44,5 +44,53 @@ public SuggestedActions(IEnumerable to, IEnumerable actions) public IList To { get; set; } /// Actions that can be shown to the user. public IList Actions { get; set; } + + /// + /// Adds a single action to and returns this instance. + /// + public SuggestedActions AddAction(CardAction action) + { + Actions ??= []; + Actions.Add(action); + return this; + } + + /// + /// Adds one or more actions to and returns this instance. + /// + public SuggestedActions AddActions(params CardAction[] actions) + { + if (actions == null) + { + return this; + } + + Actions ??= []; + foreach (var action in actions) + { + Actions.Add(action); + } + + return this; + } + + /// + /// Adds one or more recipient ids to and returns this instance. + /// + public SuggestedActions AddRecipients(params string[] recipients) + { + if (recipients == null) + { + return this; + } + + To ??= []; + foreach (var recipient in recipients) + { + To.Add(recipient); + } + + return this; + } } } diff --git a/src/libraries/Core/Microsoft.Agents.Core/Models/ThumbnailCard.cs b/src/libraries/Core/Microsoft.Agents.Core/Models/ThumbnailCard.cs index ed2540520..1fbae9178 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/Models/ThumbnailCard.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/Models/ThumbnailCard.cs @@ -8,7 +8,7 @@ namespace Microsoft.Agents.Core.Models { /// A thumbnail card (card with a single, small thumbnail image). - public class ThumbnailCard + public class ThumbnailCard : Card { public ThumbnailCard() { @@ -43,7 +43,7 @@ public ThumbnailCard(string title = default, string subtitle = default, string t /// /// The instance of . /// The generated attachment. - public Attachment ToAttachment() + public override Attachment ToAttachment() { return new Attachment { @@ -52,22 +52,6 @@ public Attachment ToAttachment() }; } - /// - /// Creates a new message activity that includes this thumbnail card as an attachment. - /// - /// Use this method to generate a message activity suitable for sending to a user, with - /// the current thumbnail card included as an attachment. The returned activity has its type set to . - /// An representing a message activity with the thumbnail card attached. - public IActivity ToMessage() - { - return new Activity - { - Type = ActivityTypes.Message, - Attachments = [ToAttachment()] - }; - } - /// Title of the card. public string Title { get; set; } /// Subtitle of the card. @@ -80,5 +64,16 @@ public IActivity ToMessage() public IList Buttons { get; set; } /// A clickable action. public CardAction Tap { get; set; } + + /// Adds an image and returns this card. + public ThumbnailCard AddImage(CardImage image) { Images ??= []; Images.Add(image); return this; } + /// Adds an image by URL and returns this card. + public ThumbnailCard AddImage(string url, string alt = null) { Images ??= []; Images.Add(new CardImage(url, alt)); return this; } + /// Adds a button and returns this card. + public ThumbnailCard AddButton(CardAction button) { Buttons ??= []; Buttons.Add(button); return this; } + /// Adds a button by title/type/value and returns this card. + public ThumbnailCard AddButton(string title, string type = ActionTypes.ImBack, object value = null) { Buttons ??= []; Buttons.Add(new CardAction(type: type, title: title, value: value ?? title)); return this; } + /// Adds one or more buttons and returns this card. + public ThumbnailCard AddButtons(params CardAction[] buttons) { if (buttons == null) { return this; } Buttons ??= []; foreach (var button in buttons) { Buttons.Add(button); } return this; } } } diff --git a/src/libraries/Core/Microsoft.Agents.Core/Models/VideoCard.cs b/src/libraries/Core/Microsoft.Agents.Core/Models/VideoCard.cs index 281852b72..125820922 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/Models/VideoCard.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/Models/VideoCard.cs @@ -9,7 +9,7 @@ namespace Microsoft.Agents.Core.Models { /// Video card. - public class VideoCard + public class VideoCard : Card { public VideoCard() { @@ -56,7 +56,7 @@ public VideoCard(string title = default, string subtitle = default, string text /// /// The instance of . /// The generated attachment. - public Attachment ToAttachment() + public override Attachment ToAttachment() { return new Attachment { @@ -65,22 +65,6 @@ public Attachment ToAttachment() }; } - /// - /// Creates a new message activity that includes this video card as an attachment. - /// - /// Use this method to generate a message activity suitable for sending to a user, with - /// the current video card included as an attachment. The returned activity has its type set to . - /// An representing a message activity with the video card attached. - public IActivity ToMessage() - { - return new Activity - { - Type = ActivityTypes.Message, - Attachments = [ToAttachment()] - }; - } - /// Title of this card. public string Title { get; set; } /// Subtitle of this card. @@ -107,5 +91,12 @@ public IActivity ToMessage() /// Supplementary parameter for this card. [JsonConverter(typeof(Serialization.Converters.ObjectTypeConverter))] public object Value { get; set; } + + /// Adds a media URL and returns this card. + public VideoCard AddMedia(MediaUrl media) { Media ??= []; Media.Add(media); return this; } + /// Adds a media URL by string and returns this card. + public VideoCard AddMedia(string url, string profile = null) { Media ??= []; Media.Add(new MediaUrl(url, profile)); return this; } + /// Adds a button and returns this card. + public VideoCard AddButton(CardAction button) { Buttons ??= []; Buttons.Add(button); return this; } } } diff --git a/src/libraries/Core/Microsoft.Agents.Core/Serialization/CoreJsonContext.cs b/src/libraries/Core/Microsoft.Agents.Core/Serialization/CoreJsonContext.cs index 8df26ba7a..6f005be30 100644 --- a/src/libraries/Core/Microsoft.Agents.Core/Serialization/CoreJsonContext.cs +++ b/src/libraries/Core/Microsoft.Agents.Core/Serialization/CoreJsonContext.cs @@ -32,9 +32,12 @@ namespace Microsoft.Agents.Core.Serialization [JsonSerializable(typeof(AdaptiveCardInvokeValue))] [JsonSerializable(typeof(AadResourceUrls))] [JsonSerializable(typeof(AIEntity))] + [JsonSerializable(typeof(AnimationCard))] [JsonSerializable(typeof(Attachment))] [JsonSerializable(typeof(AudioCard))] +#pragma warning disable CS0618 // BasicCard is obsolete but retained for serialization compatibility [JsonSerializable(typeof(BasicCard))] +#pragma warning restore CS0618 [JsonSerializable(typeof(CardImage))] [JsonSerializable(typeof(ChannelAccount))] [JsonSerializable(typeof(Citation))] @@ -50,7 +53,9 @@ namespace Microsoft.Agents.Core.Serialization [JsonSerializable(typeof(HeroCard))] [JsonSerializable(typeof(InnerHttpError))] [JsonSerializable(typeof(InvokeResponse))] +#pragma warning disable CS0618 // MediaCard is obsolete but retained for serialization compatibility [JsonSerializable(typeof(MediaCard))] +#pragma warning restore CS0618 [JsonSerializable(typeof(MediaEventValue))] [JsonSerializable(typeof(MediaUrl))] [JsonSerializable(typeof(Mention))] @@ -71,6 +76,7 @@ namespace Microsoft.Agents.Core.Serialization [JsonSerializable(typeof(SuggestedActions))] [JsonSerializable(typeof(TextHighlight))] [JsonSerializable(typeof(Thing))] + [JsonSerializable(typeof(ThumbnailCard))] [JsonSerializable(typeof(TokenExchangeInvokeRequest))] [JsonSerializable(typeof(TokenExchangeInvokeResponse))] [JsonSerializable(typeof(TokenExchangeRequest))] diff --git a/src/tests/Microsoft.Agents.Model.Tests/ActivityBuilderTests.cs b/src/tests/Microsoft.Agents.Model.Tests/ActivityBuilderTests.cs new file mode 100644 index 000000000..8df6b2aae --- /dev/null +++ b/src/tests/Microsoft.Agents.Model.Tests/ActivityBuilderTests.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.Core.Models; +using Xunit; + +namespace Microsoft.Agents.Model.Tests +{ + public class ActivityBuilderTests + { + [Fact] + public void FluentSettersChainAndSetProperties() + { + var activity = Activity.CreateMessageActivity() + .WithText("hello") + .WithSpeak("hello there") + .WithInputHint(InputHints.ExpectingInput) + .WithSummary("a summary") + .WithLocale("en-US") + .WithTextFormat(TextFormatTypes.Markdown) + .WithAttachmentLayout(AttachmentLayoutTypes.Carousel) + .WithDeliveryMode(DeliveryModes.Normal) + .WithName("theName") + .WithValue("theValue", "theValueType"); + + Assert.Equal("hello", activity.Text); + Assert.Equal("hello there", activity.Speak); + Assert.Equal(InputHints.ExpectingInput, activity.InputHint); + Assert.Equal("a summary", activity.Summary); + Assert.Equal("en-US", activity.Locale); + Assert.Equal(TextFormatTypes.Markdown, activity.TextFormat); + Assert.Equal(AttachmentLayoutTypes.Carousel, activity.AttachmentLayout); + Assert.Equal(DeliveryModes.Normal, activity.DeliveryMode); + Assert.Equal("theName", activity.Name); + Assert.Equal("theValue", activity.Value); + Assert.Equal("theValueType", activity.ValueType); + } + + [Fact] + public void AddTextAppends() + { + var activity = Activity.CreateMessageActivity().WithText("foo").AddText("bar"); + Assert.Equal("foobar", activity.Text); + } + + [Fact] + public void AddAttachmentAddsAttachments() + { + var activity = Activity.CreateMessageActivity() + .AddAttachment(new Attachment(contentType: "a"), new Attachment(contentType: "b")); + + Assert.Equal(2, activity.Attachments.Count); + Assert.Equal("a", activity.Attachments[0].ContentType); + Assert.Equal("b", activity.Attachments[1].ContentType); + } + + [Fact] + public void AddAttachmentWrapsCards() + { + var activity = Activity.CreateMessageActivity() + .AddCard(new HeroCard(title: "hero")) + .AddCard(new ThumbnailCard(title: "thumb")) + .AddCard(new OAuthCard()) + .AddCard(new SigninCard()); + + Assert.Equal(4, activity.Attachments.Count); + Assert.Equal(HeroCard.ContentType, activity.Attachments[0].ContentType); + Assert.IsType(activity.Attachments[0].Content); + Assert.Equal(ThumbnailCard.ContentType, activity.Attachments[1].ContentType); + Assert.Equal(OAuthCard.ContentType, activity.Attachments[2].ContentType); + Assert.Equal(SigninCard.ContentType, activity.Attachments[3].ContentType); + } + + [Fact] + public void AddEntityAddsEntities() + { + var activity = Activity.CreateMessageActivity().AddEntity(new Entity("myType")); + Assert.Single(activity.Entities); + Assert.Equal("myType", activity.Entities[0].Type); + } + + [Fact] + public void AddMentionAddsEntityAndText() + { + var account = new ChannelAccount(id: "u1", name: "User One"); + var activity = Activity.CreateMessageActivity().WithText("hi").AddMention(account); + + Assert.Equal("User One hi", activity.Text); + var mention = Assert.IsType(activity.Entities[0]); + Assert.Equal("u1", mention.Mentioned.Id); + Assert.Equal("User One", mention.Text); + } + + [Fact] + public void AddMentionCanSkipText() + { + var account = new ChannelAccount(id: "u1", name: "User One"); + var activity = Activity.CreateMessageActivity().WithText("hi").AddMention(account, text: "Custom", addText: false); + + Assert.Equal("hi", activity.Text); + var mention = Assert.IsType(activity.Entities[0]); + Assert.Equal("Custom", mention.Text); + } + + [Fact] + public void IsTypeHelpers() + { + Assert.True(Activity.CreateMessageActivity().IsMessage()); + Assert.True(Activity.CreateTypingActivity().IsTyping()); + Assert.True(Activity.CreateEventActivity().IsEvent()); + Assert.True(Activity.CreateInvokeActivity().IsInvoke()); + Assert.True(Activity.CreateConversationUpdateActivity().IsConversationUpdate()); + Assert.True(Activity.CreateEndOfConversationActivity().IsEndOfConversation()); + Assert.True(Activity.CreateHandoffActivity().IsHandoff()); + Assert.False(Activity.CreateMessageActivity().IsInvoke()); + } + } +} diff --git a/src/tests/Microsoft.Agents.Model.Tests/BasicCardTests.cs b/src/tests/Microsoft.Agents.Model.Tests/BasicCardTests.cs index f8e7f21ed..c4ce8fca2 100644 --- a/src/tests/Microsoft.Agents.Model.Tests/BasicCardTests.cs +++ b/src/tests/Microsoft.Agents.Model.Tests/BasicCardTests.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using Xunit; +#pragma warning disable CS0618 // Type or member is obsolete - exercising obsolete BasicCard + namespace Microsoft.Agents.Model.Tests { public class BasicCardTests diff --git a/src/tests/Microsoft.Agents.Model.Tests/BuilderHelpersTests.cs b/src/tests/Microsoft.Agents.Model.Tests/BuilderHelpersTests.cs new file mode 100644 index 000000000..ba479e531 --- /dev/null +++ b/src/tests/Microsoft.Agents.Model.Tests/BuilderHelpersTests.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Agents.Core.Models; +using Xunit; + +namespace Microsoft.Agents.Model.Tests +{ + public class BuilderHelpersTests + { + [Fact] + public void SuggestedActionsFluentAdders() + { + var suggested = new SuggestedActions() + .AddRecipients("r1", "r2") + .AddAction(new CardAction(title: "a")) + .AddActions(new CardAction(title: "b"), new CardAction(title: "c")); + + Assert.Equal(new[] { "r1", "r2" }, suggested.To); + Assert.Equal(3, suggested.Actions.Count); + Assert.Equal("a", suggested.Actions[0].Title); + Assert.Equal("c", suggested.Actions[2].Title); + } + + [Fact] + public void GetAccountMentionReturnsMatch() + { + var account = new ChannelAccount(id: "u1", name: "User One"); + var activity = Activity.CreateMessageActivity().AddMention(account); + + var mention = activity.GetAccountMention("u1"); + Assert.NotNull(mention); + Assert.Equal("u1", mention.Mentioned.Id); + + Assert.Null(activity.GetAccountMention("other")); + Assert.Null(activity.GetAccountMention(null)); + } + + [Fact] + public void IsRecipientMentioned() + { + var recipient = new ChannelAccount(id: "bot", name: "Bot"); + var activity = Activity.CreateMessageActivity(); + activity.Recipient = recipient; + + Assert.False(activity.IsRecipientMentioned()); + + activity.AddMention(recipient); + Assert.True(activity.IsRecipientMentioned()); + } + } +} diff --git a/src/tests/Microsoft.Agents.Model.Tests/CardBuilderTests.cs b/src/tests/Microsoft.Agents.Model.Tests/CardBuilderTests.cs new file mode 100644 index 000000000..6c9655215 --- /dev/null +++ b/src/tests/Microsoft.Agents.Model.Tests/CardBuilderTests.cs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.IO; +using System.Text; +using Microsoft.Agents.Core.Models; +using Microsoft.Agents.Core.Serialization; +using Xunit; + +#pragma warning disable CS0618 // Type or member is obsolete - exercising obsolete BasicCard/MediaCard builders + +namespace Microsoft.Agents.Model.Tests +{ + public class CardBuilderTests + { + [Fact] + public void HeroCardFluentBuilders() + { + var card = new HeroCard { Title = "t", Subtitle = "s", Text = "txt", Tap = new CardAction(type: ActionTypes.OpenUrl) } + .AddImage("https://img", "alt") + .AddImage(new CardImage("https://img2")) + .AddButton("Yes", value: "yes") + .AddButton(new CardAction(type: ActionTypes.PostBack, title: "No")) + .AddButtons(new CardAction(title: "A"), new CardAction(title: "B")); + + Assert.Equal("t", card.Title); + Assert.Equal("s", card.Subtitle); + Assert.Equal("txt", card.Text); + Assert.Equal(ActionTypes.OpenUrl, card.Tap.Type); + Assert.Equal(2, card.Images.Count); + Assert.Equal("https://img", card.Images[0].Url); + Assert.Equal("alt", card.Images[0].Alt); + Assert.Equal(4, card.Buttons.Count); + Assert.Equal("Yes", card.Buttons[0].Title); + Assert.Equal("yes", card.Buttons[0].Value); + } + + [Fact] + public void ThumbnailAndBasicCardBuilders() + { + var thumb = new ThumbnailCard { Title = "t" }.AddImage("u").AddButton("b"); + Assert.Equal("t", thumb.Title); + Assert.Single(thumb.Images); + Assert.Single(thumb.Buttons); + + var basic = new BasicCard { Text = "x" }.AddImage(new CardImage("u")).AddButton(new CardAction(title: "b")); + Assert.Equal("x", basic.Text); + Assert.Single(basic.Images); + Assert.Single(basic.Buttons); + } + + [Fact] + public void MediaCardBuildersAddMediaAndButtons() + { + var animation = new AnimationCard { Title = "a" }.AddMedia("https://m").AddButton(new CardAction(title: "b")); + Assert.Equal("a", animation.Title); + Assert.Single(animation.Media); + Assert.Equal("https://m", animation.Media[0].Url); + Assert.Single(animation.Buttons); + + var audio = new AudioCard().AddMedia(new MediaUrl("https://a")); + Assert.Single(audio.Media); + + var video = new VideoCard().AddMedia("https://v", "profile"); + Assert.Equal("profile", video.Media[0].Profile); + + var media = new MediaCard { Text = "m" }.AddMedia("https://x"); + Assert.Equal("m", media.Text); + Assert.Single(media.Media); + } + + [Fact] + public void ReceiptCardBuilders() + { + var receipt = new ReceiptCard { Title = "r", Total = "$10", Tax = "$1", Tap = new CardAction(type: ActionTypes.OpenUrl) } + .AddFact(new Fact("key", "value")) + .AddItem(new ReceiptItem(title: "item")) + .AddButton(new CardAction(title: "b")); + + Assert.Equal("r", receipt.Title); + Assert.Equal("$10", receipt.Total); + Assert.Equal("$1", receipt.Tax); + Assert.Equal(ActionTypes.OpenUrl, receipt.Tap.Type); + Assert.Single(receipt.Facts); + Assert.Single(receipt.Items); + Assert.Single(receipt.Buttons); + } + + [Fact] + public void AdaptiveCardCardFromJsonToAttachment() + { + var json = "{\"type\":\"AdaptiveCard\",\"version\":\"1.4\"}"; + var card = new AdaptiveCardCard(json); + + Assert.Equal(json, card.Content); + + var attachment = card.ToAttachment(); + Assert.Equal(ContentTypes.AdaptiveCard, attachment.ContentType); + Assert.Equal(json, attachment.Content); + } + + [Fact] + public void AdaptiveCardCardFromStreamToAttachment() + { + var json = "{\"type\":\"AdaptiveCard\",\"version\":\"1.4\"}"; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + var card = new AdaptiveCardCard(stream); + + Assert.Equal(json, card.Content); + // Stream is left open. + Assert.True(stream.CanRead); + } + + [Fact] + public void AdaptiveCardCardSerializesContentAsNestedJson() + { + var json = "{\"type\":\"AdaptiveCard\",\"version\":\"1.4\"}"; + var activity = ((IActivity)Activity.CreateMessageActivity()).AddCard(new AdaptiveCardCard(json)); + + var serialized = ProtocolJsonSerializer.ToJson(activity); + + // Content is unpacked into nested JSON (an object), not an escaped string. + Assert.Contains("\"content\":{\"type\":\"AdaptiveCard\"", serialized); + Assert.Equal(ContentTypes.AdaptiveCard, activity.Attachments[0].ContentType); + } + } +} diff --git a/src/tests/Microsoft.Agents.Model.Tests/IActivityExtensionsTests.cs b/src/tests/Microsoft.Agents.Model.Tests/IActivityExtensionsTests.cs index 2092d0d1f..b945e5958 100644 --- a/src/tests/Microsoft.Agents.Model.Tests/IActivityExtensionsTests.cs +++ b/src/tests/Microsoft.Agents.Model.Tests/IActivityExtensionsTests.cs @@ -118,10 +118,10 @@ public void MakeTargetedActivity_AlreadyTargeted_IsIdempotent() } [Fact] - public void MakeTargetedActivity_NullActivity_ThrowsArgumentNullException() + public void MakeTargetedActivity_NullActivity_Throws() { IActivity activity = null; - Assert.Throws(() => activity.MakeTargetedActivity()); + Assert.Throws(() => activity.MakeTargetedActivity()); } } } diff --git a/src/tests/Microsoft.Agents.Model.Tests/MediaTests.cs b/src/tests/Microsoft.Agents.Model.Tests/MediaTests.cs index 082b6f976..b7a9e8e64 100644 --- a/src/tests/Microsoft.Agents.Model.Tests/MediaTests.cs +++ b/src/tests/Microsoft.Agents.Model.Tests/MediaTests.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using Xunit; +#pragma warning disable CS0618 // Type or member is obsolete - exercising obsolete MediaCard + namespace Microsoft.Agents.Model.Tests { public class MediaTests