Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Libraries/Microsoft.Teams.Api/Account.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class Account

[JsonPropertyName("isTargeted")]
[JsonPropertyOrder(6)]
[JsonInclude]
[Experimental("ExperimentalTeamsTargeted")]
public bool? IsTargeted { get; internal set; }
}
Expand Down
35 changes: 35 additions & 0 deletions Libraries/Microsoft.Teams.Api/Activities/Activity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,41 @@ public virtual Activity AddFeedback(FeedbackType mode)
return this;
}

/// <summary>
/// add a targeted message info entity for prompt preview.
/// If an entity with type "targetedMessageInfo" already exists, it is not added again.
/// Also removes any "quotedReply" entities from <see cref="Entities"/> and strips
/// matching &lt;quoted messageId="..."/&gt; placeholders from <see cref="MessageActivity.Text"/>
/// to prevent collision between quoted replies and prompt preview.
/// </summary>
/// <param name="messageId">the message ID of the targeted message</param>
Comment thread
ShanmathiMayuramKrithivasan marked this conversation as resolved.
[Experimental("ExperimentalTeamsTargeted")]
public virtual Activity AddTargetedMessageInfo(string messageId)
{
var hasEntity = Entities?.Any(e => e.Type == "targetedMessageInfo") ?? false;
if (!hasEntity)
{
// Remove any quotedReply entities and matching <quoted .../> placeholder
// to avoid collision with prompt preview
if (Entities is not null)
{
for (var i = Entities.Count - 1; i >= 0; i--)
{
if (Entities[i].Type == "quotedReply") Entities.RemoveAt(i);
}
}

if (this is MessageActivity message && message.Text is not null)
{
message.Text = message.Text.Replace($"<quoted messageId=\"{messageId}\"/>", string.Empty).Trim();
}

Comment thread
ShanmathiMayuramKrithivasan marked this conversation as resolved.
Outdated
AddEntity(new TargetedMessageInfoEntity { MessageId = messageId });
}

return this;
}

/// <summary>
/// add a citation
/// </summary>
Expand Down
17 changes: 17 additions & 0 deletions Libraries/Microsoft.Teams.Api/Entities/Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,9 +117,15 @@
"message" or "https://schema.org/Message" => (Entity?)element.Deserialize<IMessageEntity>(options),
"ProductInfo" => element.Deserialize<ProductInfoEntity>(options),
"streaminfo" => element.Deserialize<StreamInfoEntity>(options),
<<<<<<< shmayura/prompt-preview

Check failure on line 120 in Libraries/Microsoft.Teams.Api/Entities/Entity.cs

View workflow job for this annotation

GitHub Actions / Build & Test & Lint

Merge conflict marker encountered

Check failure on line 120 in Libraries/Microsoft.Teams.Api/Entities/Entity.cs

View workflow job for this annotation

GitHub Actions / Build & Test & Lint

Merge conflict marker encountered
#pragma warning disable ExperimentalTeamsTargeted
"targetedMessageInfo" => element.Deserialize<TargetedMessageInfoEntity>(options),
#pragma warning restore ExperimentalTeamsTargeted
=======

Check failure on line 124 in Libraries/Microsoft.Teams.Api/Entities/Entity.cs

View workflow job for this annotation

GitHub Actions / Build & Test & Lint

Merge conflict marker encountered

Check failure on line 124 in Libraries/Microsoft.Teams.Api/Entities/Entity.cs

View workflow job for this annotation

GitHub Actions / Build & Test & Lint

Merge conflict marker encountered
#pragma warning disable ExperimentalTeamsQuotedReplies
"quotedReply" => element.Deserialize<QuotedReplyEntity>(options),
#pragma warning restore ExperimentalTeamsQuotedReplies
>>>>>>> main

Check failure on line 128 in Libraries/Microsoft.Teams.Api/Entities/Entity.cs

View workflow job for this annotation

GitHub Actions / Build & Test & Lint

Merge conflict marker encountered

Check failure on line 128 in Libraries/Microsoft.Teams.Api/Entities/Entity.cs

View workflow job for this annotation

GitHub Actions / Build & Test & Lint

Merge conflict marker encountered
_ => null
};
Comment thread
ShanmathiMayuramKrithivasan marked this conversation as resolved.

Expand Down Expand Up @@ -164,6 +170,16 @@
return;
}

<<<<<<< shmayura/prompt-preview

Check failure on line 173 in Libraries/Microsoft.Teams.Api/Entities/Entity.cs

View workflow job for this annotation

GitHub Actions / Build & Test & Lint

Merge conflict marker encountered

Check failure on line 173 in Libraries/Microsoft.Teams.Api/Entities/Entity.cs

View workflow job for this annotation

GitHub Actions / Build & Test & Lint

Merge conflict marker encountered
#pragma warning disable ExperimentalTeamsTargeted
if (value is TargetedMessageInfoEntity targetedMessageInfo)
{
JsonSerializer.Serialize(writer, targetedMessageInfo, options);
return;
}
#pragma warning restore ExperimentalTeamsTargeted

=======

Check failure on line 182 in Libraries/Microsoft.Teams.Api/Entities/Entity.cs

View workflow job for this annotation

GitHub Actions / Build & Test & Lint

Merge conflict marker encountered
#pragma warning disable ExperimentalTeamsQuotedReplies
if (value is QuotedReplyEntity quotedReply)
{
Expand All @@ -172,6 +188,7 @@
}

#pragma warning restore ExperimentalTeamsQuotedReplies
>>>>>>> main

Check failure on line 191 in Libraries/Microsoft.Teams.Api/Entities/Entity.cs

View workflow job for this annotation

GitHub Actions / Build & Test & Lint

Merge conflict marker encountered
JsonSerializer.Serialize(writer, value.ToJsonObject(options), options);
}
Comment thread
ShanmathiMayuramKrithivasan marked this conversation as resolved.
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Serialization;

namespace Microsoft.Teams.Api.Entities;

Comment thread
ShanmathiMayuramKrithivasan marked this conversation as resolved.
[Experimental("ExperimentalTeamsTargeted")]
public class TargetedMessageInfoEntity : Entity
Comment thread
ShanmathiMayuramKrithivasan marked this conversation as resolved.
{
[JsonPropertyName("messageId")]
[JsonPropertyOrder(3)]
public required string MessageId { get; set; }

public TargetedMessageInfoEntity() : base("targetedMessageInfo") { }
}
9 changes: 9 additions & 0 deletions Libraries/Microsoft.Teams.Apps/Contexts/Context.Send.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ public partial class Context<TActivity> : IContext<TActivity>
{
public async Task<T> Send<T>(T activity, CancellationToken cancellationToken = default) where T : IActivity
{
// Auto-populate targetedMessageInfo entity for prompt preview
// when the incoming activity was a targeted message (reactive flow).
#pragma warning disable ExperimentalTeamsTargeted
if (activity is MessageActivity messageActivity && Activity.Recipient?.IsTargeted == true && Activity.Id is not null)
{
messageActivity.AddTargetedMessageInfo(Activity.Id);
Comment thread
ShanmathiMayuramKrithivasan marked this conversation as resolved.
}
#pragma warning restore ExperimentalTeamsTargeted
Comment thread
ShanmathiMayuramKrithivasan marked this conversation as resolved.

var res = await Sender.Send(activity, Ref, CancellationToken);
Comment thread
ShanmathiMayuramKrithivasan marked this conversation as resolved.
await OnActivitySent(res, ToActivityType<IActivity>());
return res;
Expand Down
53 changes: 24 additions & 29 deletions Samples/Samples.TargetedMessages/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
using Microsoft.Teams.Plugins.AspNetCore.DevTools.Extensions;
using Microsoft.Teams.Plugins.AspNetCore.Extensions;

#pragma warning disable ExperimentalTeamsTargeted

var builder = WebApplication.CreateBuilder(args);
builder.AddTeams().AddTeamsDevTools();
var app = builder.Build();
Expand All @@ -25,23 +27,7 @@

context.Log.Info($"[MESSAGE] Received: {text}");

if (text.Contains("send"))
{
var members = await context.Api.Conversations.Members.GetAsync(activity.Conversation.Id, cancellationToken);

foreach (var member in members)
{
context.Log.Info($"[MEMBER] {member.Name} (ID: {member.Id})");

// SEND: Create a new targeted message
await context.Send(
new MessageActivity($"👋 {member.Name} This is a **targeted message** - only YOU can see this!")
.WithRecipient(new Account() { Id = member.Id, Name = member.Name, Role = Role.User }, true), cancellationToken);
}

context.Log.Info($"[SEND] Sent targeted message");
}
else if (text.Contains("update"))
if (text.Contains("test update"))
{
// UPDATE: Send a targeted message, then update it after 3 seconds
var conversationId = activity.Conversation?.Id ?? "";
Expand Down Expand Up @@ -75,7 +61,7 @@ await context.Send(

context.Log.Info($"[UPDATE] Scheduled update in 3 seconds");
}
else if (text.Contains("delete"))
else if (text.Contains("test delete"))
{
// DELETE: Send a targeted message, then delete it after 3 seconds
var conversationId = activity.Conversation?.Id ?? "";
Expand Down Expand Up @@ -107,29 +93,38 @@ await context.Send(

context.Log.Info($"[DELETE] Scheduled delete in 3 seconds");
}
else if (text.Contains("reply"))
else if (text.Contains("test public"))
{
// REPLY: Send a targeted reply to the user's message
await context.Reply(
new MessageActivity("💬 This is a **targeted reply** - threaded and private!")
.WithRecipient(context.Activity.From, true), cancellationToken);
// PUBLIC: Send a public message visible to everyone in the chat.
await context.Send(
new MessageActivity("📋 Here is the public result — everyone can see this!"),
cancellationToken);

context.Log.Info("[PUBLIC] Sent public message");
}
else if (text.Contains("test send"))
{
// SEND: Send a targeted message visible only to the sender.
await context.Send(
new MessageActivity("👋 This is a **targeted message** — only YOU can see this!")
.WithRecipient(context.Activity.From, true),
cancellationToken);

context.Log.Info("[REPLY] Sent targeted reply");
context.Log.Info("[SEND] Sent targeted message");
}
else if (text.Contains("help"))
{
await context.Send(
"**🎯 Targeted Messages Demo**\n\n" +
"**Commands:**\n" +
"- `send` - Send a targeted message (only you see it)\n" +
"- `update` - Send a message, then update it after 3 seconds\n" +
"- `delete` - Send a message, then delete it after 3 seconds\n" +
"- `reply` - Get a targeted reply (threaded)\n\n" +
"- `test send` - Send a targeted message (only visible to you)\n" +
"- `test update` - Send a targeted message, then update it after 3 seconds\n" +
"- `test delete` - Send a targeted message, then delete it after 3 seconds\n" +
"- `test public` - Send a public reply (visible to all)\n\n" +
"_Targeted messages are only visible to you, even in group chats!_", cancellationToken);
}
else
{
await context.Typing(null, cancellationToken);
await context.Send($"You said: '{activity.Text}'\n\nType `help` to see available commands.", cancellationToken);
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System.Text.Json;

using Microsoft.Teams.Api.Activities;
using Microsoft.Teams.Api.Entities;

namespace Microsoft.Teams.Api.Tests.Entities;

#pragma warning disable ExperimentalTeamsTargeted
public class TargetedMessageInfoEntityTests
{
[Fact]
public void TargetedMessageInfoEntity_JsonSerialize()
{
var entity = new TargetedMessageInfoEntity()
{
MessageId = "1772129782775"
};

var json = JsonSerializer.Serialize(entity, new JsonSerializerOptions()
{
WriteIndented = true,
IndentSize = 2,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
});

Assert.Equal(File.ReadAllText(
@"../../../Json/Entities/TargetedMessageInfoEntity.json"
), json);
}

[Fact]
public void TargetedMessageInfoEntity_JsonSerialize_Derived()
{
Entity entity = new TargetedMessageInfoEntity()
{
MessageId = "1772129782775"
};

var json = JsonSerializer.Serialize(entity, new JsonSerializerOptions()
{
WriteIndented = true,
IndentSize = 2,
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull
});

Assert.Equal(File.ReadAllText(
@"../../../Json/Entities/TargetedMessageInfoEntity.json"
), json);
}

[Fact]
public void TargetedMessageInfoEntity_JsonDeserialize()
{
var json = File.ReadAllText(@"../../../Json/Entities/TargetedMessageInfoEntity.json");
var entity = JsonSerializer.Deserialize<TargetedMessageInfoEntity>(json);

Assert.NotNull(entity);
Assert.Equal("targetedMessageInfo", entity.Type);
Assert.Equal("1772129782775", entity.MessageId);
}

[Fact]
public void TargetedMessageInfoEntity_JsonDeserialize_Derived()
{
var json = File.ReadAllText(@"../../../Json/Entities/TargetedMessageInfoEntity.json");
var entity = JsonSerializer.Deserialize<Entity>(json);

Assert.NotNull(entity);
Assert.IsType<TargetedMessageInfoEntity>(entity);

var targeted = (TargetedMessageInfoEntity)entity;
Assert.Equal("targetedMessageInfo", targeted.Type);
Assert.Equal("1772129782775", targeted.MessageId);
}

[Fact]
public void AddTargetedMessageInfo_AddsEntity()
{
var activity = new MessageActivity("test");
activity.AddTargetedMessageInfo("12345");

var entity = activity.Entities?.OfType<TargetedMessageInfoEntity>().SingleOrDefault();
Assert.NotNull(entity);
Assert.Equal("12345", entity!.MessageId);
}

[Fact]
public void AddTargetedMessageInfo_DoesNotDuplicate_WhenConcreteEntityExists()
{
var activity = new MessageActivity("test")
.AddEntity(new TargetedMessageInfoEntity { MessageId = "9999" });

activity.AddTargetedMessageInfo("12345");

var entities = activity.Entities!.OfType<TargetedMessageInfoEntity>().ToList();
Assert.Single(entities);
Assert.Equal("9999", entities[0].MessageId);
}

[Fact]
public void AddTargetedMessageInfo_DoesNotDuplicate_WhenGenericEntityWithMatchingType()
{
var activity = new MessageActivity("test")
.AddEntity(new Entity("targetedMessageInfo"));

activity.AddTargetedMessageInfo("12345");

var entities = activity.Entities!.Where(e => e.Type == "targetedMessageInfo").ToList();
Assert.Single(entities);
}

[Fact]
public void AddTargetedMessageInfo_StripsQuotedReplyEntities()
{
var activity = new MessageActivity("test")
.AddEntity(new Entity("quotedReply"));

activity.AddTargetedMessageInfo("12345");

Assert.DoesNotContain(activity.Entities!, e => e.Type == "quotedReply");
Assert.Contains(activity.Entities!, e => e.Type == "targetedMessageInfo");
}

[Fact]
public void AddTargetedMessageInfo_StripsQuotedPlaceholderFromText()
{
var activity = new MessageActivity("<quoted messageId=\"12345\"/> Here is my reply");

activity.AddTargetedMessageInfo("12345");

Assert.Equal("Here is my reply", activity.Text);
Assert.Contains(activity.Entities!, e => e.Type == "targetedMessageInfo");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "targetedMessageInfo",
"messageId": "1772129782775"
}
Loading
Loading