Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
10 changes: 0 additions & 10 deletions Libraries/Microsoft.Teams.Api/Activities/Activity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -233,16 +233,6 @@ public virtual Activity WithRecipient(Account value)
#pragma warning restore ExperimentalTeamsTargeted
}

[Experimental("ExperimentalTeamsTargeted")]
public virtual Activity WithRecipient(Account value, bool isTargeted)
{
Recipient = value;
#pragma warning disable ExperimentalTeamsTargeted
Recipient.IsTargeted = null;
#pragma warning restore ExperimentalTeamsTargeted
return this;
}

[Experimental("ExperimentalTeamsTargeted")]
public virtual Activity WithRecipient(Account value, bool isTargeted)
{
Expand Down
1 change: 1 addition & 0 deletions core/core.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
<Project Path="samples/MeetingsBot/MeetingsBot.csproj" />
<Project Path="samples/MessageExtensionBot/MessageExtensionBot.csproj" />
<Project Path="samples/PABot/PABot.csproj" Id="ef8f29ef-fe59-4edf-8a50-6e7ab6699a45" />
<Project Path="samples/Quoting/Quoting.csproj" />
<Project Path="samples/StreamingBot/StreamingBot.csproj" />
<Project Path="samples/TabApp/TabApp.csproj" />
<Project Path="samples/TeamsBot/TeamsBot.csproj" Id="94a35050-6826-446f-9b29-863f2bbc75b7" />
Expand Down
116 changes: 116 additions & 0 deletions core/samples/Quoting/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.Teams.Bot.Apps;
using Microsoft.Teams.Bot.Apps.Handlers;
using Microsoft.Teams.Bot.Apps.Schema;
using Microsoft.Teams.Bot.Apps.Schema.Entities;

WebApplicationBuilder webAppBuilder = WebApplication.CreateSlimBuilder(args);
webAppBuilder.Services.AddTeamsBotApplication();
WebApplication webApp = webAppBuilder.Build();

TeamsBotApplication teamsApp = webApp.UseTeamsBotApplication();

teamsApp.OnMessage(async (context, cancellationToken) =>
{
var activity = context.Activity;
var text = activity.Text?.ToLowerInvariant()?.Trim() ?? "";

// Read inbound quoted replies
var quotes = activity.GetQuotedMessages().ToList();
if (quotes.Count > 0)
{
var quote = quotes[0].QuotedReply;
var info = $"Quoted message ID: {quote?.MessageId}";
if (quote?.SenderName != null) info += $"\nFrom: {quote.SenderName}";
if (quote?.Preview != null) info += $"\nPreview: \"{quote.Preview}\"";
if (quote?.IsReplyDeleted == true) info += "\n(deleted)";
if (quote?.ValidatedMessageReference == true) info += "\n(validated)";

await context.SendActivityAsync(
new MessageActivity($"You sent a message with a quoted reply:\n\n{info}") { TextFormat = TextFormats.Markdown },
cancellationToken);
return;
}

// ReplyAsync() — auto-quotes the inbound message
if (text.Contains("test reply"))
{
await context.ReplyAsync("Thanks for your message! This reply auto-quotes it.", cancellationToken);
return;
}

// QuoteAsync() — quote a previously sent message by ID
if (text.Contains("test quote"))
{
var sent = await context.SendActivityAsync("The meeting has been moved to 3 PM tomorrow.", cancellationToken);
if (sent?.Id != null)
{
await context.QuoteAsync(sent.Id, "Just to confirm — does the new time work for everyone?", cancellationToken);
}
return;
}

// AddQuote() extension — builder with response
if (text.Contains("test add"))
{
var sent = await context.SendActivityAsync("Please review the latest PR before end of day.", cancellationToken);
if (sent?.Id != null)
{
MessageActivity msg = new();
msg.AddQuote(sent.Id, "Done! Left my comments on the PR.");
await context.SendActivityAsync(msg, cancellationToken);
}
return;
}

// Multi-quote with mixed responses
if (text.Contains("test multi"))
{
var sentA = await context.SendActivityAsync("We need to update the API docs before launch.", cancellationToken);
var sentB = await context.SendActivityAsync("The design mockups are ready for review.", cancellationToken);
var sentC = await context.SendActivityAsync("CI pipeline is green on main.", cancellationToken);

if (sentA?.Id != null && sentB?.Id != null && sentC?.Id != null)
{
MessageActivity msg = new();
msg.AddQuote(sentA.Id, "I can take the docs — will have a draft by Thursday.");
msg.AddQuote(sentB.Id, "Looks great, approved!");
msg.AddQuote(sentC.Id);
await context.SendActivityAsync(msg, cancellationToken);
}
return;
}

// Builder pattern — WithQuote on TeamsActivityBuilder
if (text.Contains("test builder"))
{
var sent = await context.SendActivityAsync("Deployment to staging is complete.", cancellationToken);
if (sent?.Id != null)
{
TeamsActivity reply = TeamsActivity.CreateBuilder()
.WithType(TeamsActivityType.Message)
.WithQuote(sent.Id, "Verified — all smoke tests passing.")
.Build();
await context.SendActivityAsync(reply, cancellationToken);
}
return;
}

// Help / Default
await context.SendActivityAsync(
new MessageActivity(
"**Quoting Test Bot**\n\n" +
"**Commands:**\n" +
"- `test reply` - ReplyAsync() auto-quotes your message\n" +
"- `test quote` - QuoteAsync() quotes a previously sent message\n" +
"- `test add` - AddQuote() extension with response\n" +
"- `test multi` - Multi-quote with mixed responses\n" +
"- `test builder` - WithQuote() on TeamsActivityBuilder\n\n" +
"Quote any message to me to see the parsed metadata!")
{ TextFormat = TextFormats.Markdown },
cancellationToken);
});

webApp.Run();
14 changes: 14 additions & 0 deletions core/samples/Quoting/Quoting.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<NoWarn>$(NoWarn);ExperimentalTeamsQuotedReplies</NoWarn>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.Teams.Bot.Apps\Microsoft.Teams.Bot.Apps.csproj" />
</ItemGroup>

</Project>
29 changes: 29 additions & 0 deletions core/samples/Quoting/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Quoting Sample

Demonstrates various ways to quote previous messages in a Teams bot using the `quotedReply` entity.

## Prerequisites

- Bot registered and installed in a chat or channel

---

## Commands

| Command | Behavior |
|---------|----------|
| `test reply` | `ReplyAsync()` — auto-quotes the inbound message |
| `test quote` | `QuoteAsync()` — sends a message, then quotes it by ID |
| `test add` | `AddQuote()` — sends a message, then quotes it with extension method + response |
| `test multi` | Sends three messages, then quotes all with interleaved responses |
| `test builder` | `WithQuote()` on `TeamsActivityBuilder` |
| *(quote a message)* | Bot reads and displays the quoted reply metadata |

---

## Running the Sample

1. Build and run:
```bash
dotnet run --project samples/Quoting/Quoting.csproj
```
9 changes: 9 additions & 0 deletions core/samples/Quoting/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.Teams": "Information"
}
},
"AllowedHosts": "*"
}
65 changes: 65 additions & 0 deletions core/src/Microsoft.Teams.Bot.Apps/Context.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;
using Microsoft.Teams.Bot.Apps.Schema;
using Microsoft.Teams.Bot.Apps.Schema.Entities;
using Microsoft.Teams.Bot.Core;

namespace Microsoft.Teams.Bot.Apps;
Expand Down Expand Up @@ -50,6 +52,69 @@ public class Context<TActivity>(TeamsBotApplication botApplication, TActivity ac
.Build(), cancellationToken);


/// <summary>
/// Sends a message activity as a reply, automatically quoting the inbound message.
/// </summary>
/// <param name="text">The text to send.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>The response from sending the activity.</returns>
public Task<SendActivityResponse?> ReplyAsync(string text, CancellationToken cancellationToken = default)
{
var reply = new MessageActivity(text);
return ReplyAsync(reply, cancellationToken);
}

/// <summary>
/// Sends an activity as a reply, automatically quoting the inbound message.
/// </summary>
/// <param name="activity">The activity to send.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>The response from sending the activity.</returns>
public Task<SendActivityResponse?> ReplyAsync(TeamsActivity activity, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(activity);
if (Activity.Id != null)
{
return QuoteAsync(Activity.Id, activity, cancellationToken);
}

return SendActivityAsync(activity, cancellationToken);
}

/// <summary>
/// Send a message to the conversation with a quoted message reference prepended to the text.
/// Teams renders the quoted message as a preview bubble above the response text.
/// </summary>
/// <param name="messageId">The ID of the message to quote.</param>
/// <param name="text">The response text, appended to the quoted message placeholder.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>The response from sending the activity.</returns>
[Experimental("ExperimentalTeamsQuotedReplies")]
public Task<SendActivityResponse?> QuoteAsync(string messageId, string text, CancellationToken cancellationToken = default)
{
var reply = new MessageActivity(text);
return QuoteAsync(messageId, reply, cancellationToken);
}

/// <summary>
/// Send a message to the conversation with a quoted message reference prepended to the text.
/// Teams renders the quoted message as a preview bubble above the response text.
/// </summary>
/// <param name="messageId">The ID of the message to quote.</param>
/// <param name="activity">The activity to send — a quote placeholder for messageId will be prepended to its text.</param>
/// <param name="cancellationToken">Optional cancellation token.</param>
/// <returns>The response from sending the activity.</returns>
[Experimental("ExperimentalTeamsQuotedReplies")]
public Task<SendActivityResponse?> QuoteAsync(string messageId, TeamsActivity activity, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(activity);
if (activity is MessageActivity message)
{
message.PrependQuote(messageId);
}
return SendActivityAsync(activity, cancellationToken);
}

/// <summary>
/// Sends a typing activity to the conversation asynchronously.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<NoWarn>$(NoWarn);ExperimentalTeamsQuotedReplies</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Diagnostics.CodeAnalysis;

namespace Microsoft.Teams.Bot.Apps.Schema.Entities;

/// <summary>
/// Extension methods for Activity to handle quoted replies.
/// </summary>
[Experimental("ExperimentalTeamsQuotedReplies")]
public static class ActivityQuotedReplyExtensions
{
/// <summary>
/// Gets all quoted reply entities from the activity's entity collection.
/// </summary>
/// <param name="activity">The activity to extract quoted replies from. Cannot be null.</param>
/// <returns>An enumerable of QuotedReplyEntity instances found in the activity's entities.</returns>
public static IEnumerable<QuotedReplyEntity> GetQuotedMessages(this TeamsActivity activity)
{
ArgumentNullException.ThrowIfNull(activity);
if (activity.Entities == null)
{
return [];
}
return activity.Entities.Where(e => e is QuotedReplyEntity).Cast<QuotedReplyEntity>();
}

/// <summary>
/// Add a quoted message reference and append a placeholder to the message text.
/// Teams renders the quoted message as a preview bubble above the response text.
/// If text is provided, it is appended to the quoted message placeholder.
/// </summary>
/// <param name="activity">The message activity to add the quote to. Cannot be null.</param>
/// <param name="messageId">The ID of the message to quote. Cannot be null or whitespace.</param>
/// <param name="text">Optional text, appended to the quoted message placeholder.</param>
/// <returns>The created QuotedReplyEntity that was added to the activity.</returns>
public static QuotedReplyEntity AddQuote(this MessageActivity activity, string messageId, string? text = null)
{
ArgumentNullException.ThrowIfNull(activity);
ArgumentException.ThrowIfNullOrWhiteSpace(messageId);

QuotedReplyEntity entity = new() { QuotedReply = new QuotedReplyData { MessageId = messageId } };
activity.Entities ??= [];
activity.Entities.Add(entity);

var placeholder = $"<quoted messageId=\"{messageId}\"/>";
activity.Text = (activity.Text ?? "") + placeholder;
if (text != null)
{
activity.Text += $" {text}";
}

activity.Rebase();
return entity;
}

/// <summary>
/// Prepend a QuotedReply entity and placeholder before existing text.
/// Used by ReplyAsync()/QuoteAsync() for quote-above-response.
/// </summary>
/// <param name="activity">The message activity to prepend the quoted reply to.</param>
/// <param name="messageId">The ID of the message to quote.</param>
public static void PrependQuote(this MessageActivity activity, string messageId)
{
ArgumentNullException.ThrowIfNull(activity);
ArgumentException.ThrowIfNullOrWhiteSpace(messageId);

activity.Entities ??= [];
activity.Entities.Add(new QuotedReplyEntity { QuotedReply = new QuotedReplyData { MessageId = messageId } });
var placeholder = $"<quoted messageId=\"{messageId}\"/>";
var text = activity.Text?.Trim() ?? "";
activity.Text = string.IsNullOrEmpty(text) ? placeholder : $"{placeholder} {text}";
activity.Rebase();
}
}
Comment thread
corinagum marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ public class EntityList : List<Entity>
"message" or "https://schema.org/Message" => DeserializeMessageEntity(item, options),
"ProductInfo" => item.Deserialize<ProductInfoEntity>(options),
"streaminfo" => item.Deserialize<StreamInfoEntity>(options),
"quotedReply" => item.Deserialize<QuotedReplyEntity>(options),
_ => null
};
if (entity != null)
Expand Down
Loading
Loading