Skip to content
Closed
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
13 changes: 6 additions & 7 deletions samples/InteractingWithMessagesBot/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,16 @@ await context.SendAsync(
**Interacting with Messages**

**Quoting:**
- `quote reply` - auto-quote your message
- `quote reply` - quote your incoming message
- `quote message` - quote a previously sent message
- `quote add` - compose a quote with the message builder
- `quote batch` - combine multiple quotes
- `quote manual` - combine a quote and text manually

**Threading:**
- `thread reply` - send a reactive threaded reply
- `thread send` - send to the same thread without quoting
- `default send` - send to the same thread without quoting
- `thread proactive` - send a proactive threaded reply
- `thread manual` - construct a threaded conversation ID manually
- `thread proactive quote` - explicitly place and quote a threaded reply
- `thread proactive targeted` - send a proactive targeted threaded reply
- `thread proactive targeted quote` - send a proactive targeted threaded reply with a quote

**Reactions:**
- `reaction add <type>` - add a reaction to your message
Expand All @@ -57,7 +56,7 @@ Quote or react to one of my messages to see the corresponding inbound event.
string text = context.Activity.TextWithoutMentions ?? "";
if (Regex.IsMatch(
text,
@"(?i)^(help|quote (reply|message|add|batch|manual)|thread (send|reply|proactive|manual)|reaction (add \S+|remove \S+|proactive))$"))
@"(?i)^(help|quote (reply|message|batch)|default send|thread proactive( quote| targeted( quote)?)?|reaction (add \S+|remove \S+|proactive))$"))
{
return;
}
Expand Down
37 changes: 6 additions & 31 deletions samples/InteractingWithMessagesBot/QuotingHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ internal static void Register(TeamsBotApplication teamsApp)
{
teamsApp.OnMessage("(?i)^quote reply$", async (context, cancellationToken) =>
{
await context.ReplyAsync("Thanks for your message! This reply auto-quotes it.", cancellationToken);
await context.SendAsync(
new MessageActivityInput()
.AddQuote(context.Activity.Id!, "Thanks for your message!"),
cancellationToken);
});

teamsApp.OnMessage("(?i)^quote message$", async (context, cancellationToken) =>
Expand All @@ -21,23 +24,10 @@ internal static void Register(TeamsBotApplication teamsApp)
"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);
}
});

teamsApp.OnMessage("(?i)^quote add$", async (context, cancellationToken) =>
{
SendActivityResponse? sent = await context.SendAsync(
"Please review the latest PR before end of day.",
cancellationToken);
if (sent?.Id != null)
{
await context.SendAsync(
new MessageActivityInput().AddQuote(sent.Id, "Done! Left my comments on the PR."),
new MessageActivityInput()
.AddQuote(sent.Id, "Just to confirm - does the new time work for everyone?"),
cancellationToken);
}
});
Expand All @@ -63,21 +53,6 @@ await context.SendAsync(
await context.SendAsync(message, cancellationToken);
}
});

teamsApp.OnMessage("(?i)^quote manual$", async (context, cancellationToken) =>
{
SendActivityResponse? sent = await context.SendAsync(
"Deployment to staging is complete.",
cancellationToken);
if (sent?.Id != null)
{
await context.SendAsync(
new MessageActivityInput()
.AddQuote(sent.Id)
.AddText(" Verified - all smoke tests passing."),
cancellationToken);
}
});
}

internal static async Task<bool> HandleQuotedMessageAsync(
Expand Down
17 changes: 8 additions & 9 deletions samples/InteractingWithMessagesBot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Demonstrates quoting, threading, and reactions in one bot while keeping each con
in a separate handler class.

- `QuotingHandlers.cs` - quoted-message metadata and quote composition
- `ThreadingHandlers.cs` - reactive, proactive, and manually constructed threads
- `ThreadingHandlers.cs` - default and explicit thread placement
- `ReactionHandlers.cs` - reactions on inbound messages and a proactive reaction flow
- `Program.cs` - app setup, handler registration, and help

Expand All @@ -14,21 +14,20 @@ in a separate handler class.

| Command | Behavior |
|---------|----------|
| `quote reply` | `context.ReplyAsync()` auto-quotes the inbound message |
| `quote message` | `context.QuoteAsync()` quotes a previously sent message by ID |
| `quote add` | `AddQuote()` composes a quote with a response |
| `quote reply` | `MessageActivityInput.AddQuote()` quotes the inbound message |
| `quote message` | `MessageActivityInput.AddQuote()` quotes a previously sent message by ID |
| `quote batch` | Combines multiple quotes with mixed responses |
| `quote manual` | Combines `AddQuote()` and `AddText()` manually |
| *(quote a message)* | Displays the quoted-message metadata |

### Threading

| Command | Behavior |
|---------|----------|
| `thread reply` | `teamsApp.ReplyAsync()` sends a reactive threaded reply |
| `thread send` | `context.SendAsync()` sends to the same thread without quoting |
| `thread proactive` | `teamsApp.ReplyAsync()` sends a proactive threaded reply |
| `thread manual` | `ToThreadedConversationId()` and `teamsApp.SendAsync()` provide manual control |
| `default send` | `context.SendAsync()` uses the default placement for the current scope without quoting |
| `thread proactive` | `GetProactiveThreadReference()` resolves placement and `teamsApp.ReplyAsync()` sends a proactive threaded reply |
| `thread proactive quote` | `teamsApp.ReplyAsync()` explicitly places a reply and `AddQuote()` quotes the inbound message |
| `thread proactive targeted` | `teamsApp.ReplyAsync()` sends a proactive targeted reply through the explicit reply endpoint |
| `thread proactive targeted quote` | `teamsApp.ReplyAsync()` sends a proactive targeted reply with an explicit quote |

### Reactions

Expand Down
56 changes: 31 additions & 25 deletions samples/InteractingWithMessagesBot/ThreadingHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,63 @@
// Licensed under the MIT License.

using Microsoft.Teams.Apps;
using Microsoft.Teams.Core;
using Microsoft.Teams.Core.Schema;
using Microsoft.Teams.Apps.Schema;

internal static class ThreadingHandlers
{
internal static void Register(TeamsBotApplication teamsApp)
{
teamsApp.OnMessage("(?i)^thread send$", async (context, cancellationToken) =>
teamsApp.OnMessage("(?i)^default send$", async (context, cancellationToken) =>
{
await context.SendAsync("This is sent to the same thread, without quoting.", cancellationToken);
});

teamsApp.OnMessage("(?i)^thread reply$", async (context, cancellationToken) =>
teamsApp.OnMessage("(?i)^thread proactive$", async (context, cancellationToken) =>
{
(string conversationId, string threadRootId) = GetThreadReference(context.Activity);
(string conversationId, string threadRootId) = context.Activity.GetProactiveThreadReference();
await teamsApp.ReplyAsync(
conversationId,
threadRootId,
"This is a threaded reply to your message.",
"This is a proactive threaded reply using teamsApp.ReplyAsync().",
cancellationToken: cancellationToken);
});

teamsApp.OnMessage("(?i)^thread proactive$", async (context, cancellationToken) =>
teamsApp.OnMessage("(?i)^thread proactive quote$", async (context, cancellationToken) =>
{
(string conversationId, string threadRootId) = GetThreadReference(context.Activity);
(string conversationId, string threadRootId) = context.Activity.GetProactiveThreadReference();
await teamsApp.ReplyAsync(
conversationId,
threadRootId,
"This is a proactive threaded reply using teamsApp.ReplyAsync().",
new MessageActivityInput().AddQuote(
context.Activity.Id!,
"This is explicitly placed in the thread and quotes your message."),
cancellationToken: cancellationToken);
});

teamsApp.OnMessage("(?i)^thread manual$", async (context, cancellationToken) =>
teamsApp.OnMessage("(?i)^thread proactive targeted$", async (context, cancellationToken) =>
{
(string conversationId, string threadRootId) = GetThreadReference(context.Activity);
string threadId = ConversationExtensions.ToThreadedConversationId(conversationId, threadRootId);
await teamsApp.SendAsync(
threadId,
"This was sent using ToThreadedConversationId() + teamsApp.SendAsync() for manual control.",
ArgumentNullException.ThrowIfNull(context.Activity.From);
(string conversationId, string threadRootId) = context.Activity.GetProactiveThreadReference();
await teamsApp.ReplyAsync(
conversationId,
threadRootId,
new MessageActivityInput()
.WithText("This proactive targeted message uses the explicit reply endpoint.")
.WithRecipient(context.Activity.From, isTargeted: true),
cancellationToken: cancellationToken);
});
}

private static (string ConversationId, string ThreadRootId) GetThreadReference(CoreActivity activity)
{
ArgumentNullException.ThrowIfNull(activity.Conversation);
ArgumentException.ThrowIfNullOrEmpty(activity.Id);

string conversationId = activity.Conversation.Id;
string[] threadParts = conversationId.Split(";messageid=");
string threadRootId = threadParts.Length > 1 ? threadParts[1] : activity.Id;
return (conversationId, threadRootId);
teamsApp.OnMessage("(?i)^thread proactive targeted quote$", async (context, cancellationToken) =>
{
ArgumentNullException.ThrowIfNull(context.Activity.From);
(string conversationId, string threadRootId) = context.Activity.GetProactiveThreadReference();
await teamsApp.ReplyAsync(
conversationId,
threadRootId,
new MessageActivityInput()
.AddQuote(context.Activity.Id!, "This proactive targeted reply quotes your message.")
.WithRecipient(context.Activity.From, isTargeted: true),
cancellationToken: cancellationToken);
});
}
}
4 changes: 3 additions & 1 deletion samples/M365ExtensionsBot/MyTeamsBot.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ public MyTeamsBot(ApiClient api, IHttpContextAccessor accessor, ILogger<MyTeamsB

this.OnMessage("quote", async (context, ct) =>
{
await context.ReplyAsync("Quoting your message!", ct);
await context.SendAsync(
new MessageActivityInput().AddQuote(context.Activity.Id!, "Quoting your message!"),
ct);
});

this.OnMessage("targeted", async (context, ct) =>
Expand Down
7 changes: 3 additions & 4 deletions samples/TargetedMessages/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,14 @@
await context.SendAsync(reply, cancellationToken);
});

// Targeted reply to the inbound message: same wire format as send, but goes through
// Context.Reply which prepends a quoted reference to the inbound message.
// Targeted message that explicitly quotes the inbound message.
teamsApp.OnMessage("(?i)^test reply$", async (context, cancellationToken) =>
{
MessageActivityInput reply = new MessageActivityInput()
.WithText("🔒 Targeted reply visible only to you.")
.AddQuote(context.Activity.Id!, "🔒 Targeted reply visible only to you.")
.WithRecipient(context.Activity.From!, isTargeted: true)
;
await context.ReplyAsync(reply, cancellationToken);
await context.SendAsync(reply, cancellationToken);
});

// Send → Update a targeted message after 3 seconds.
Expand Down
15 changes: 11 additions & 4 deletions src/Microsoft.Teams.Apps/Clients/ActivityClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,7 @@ public Task<UpdateActivityResponse> UpdateAsync(string conversationId, string id
public Task<SendActivityResponse?> ReplyAsync(string conversationId, string id, TeamsActivityInput activity, Dictionary<string, string>? additionalHeaders = null, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(activity);
activity.ReplyToId = id;
return SendCoreAsync(conversationId, activity, isTargeted: false, additionalHeaders, cancellationToken);
return _client.ReplyToActivityAsync(conversationId, id, activity, _serviceUrl, isTargeted: false, requestContext: AgenticContext, customHeaders: additionalHeaders, cancellationToken: cancellationToken);
}

/// <summary>
Expand All @@ -95,8 +94,7 @@ public Task<UpdateActivityResponse> UpdateAsync(string conversationId, string id
{
ArgumentNullException.ThrowIfNull(activity);
CoreActivityInput input = CoreActivityInput.FromActivity(activity);
input.ReplyToId = id;
return SendCoreAsync(conversationId, input, isTargeted: false, additionalHeaders, cancellationToken);
return _client.ReplyToActivityAsync(conversationId, id, input, _serviceUrl, isTargeted: false, requestContext: AgenticContext, customHeaders: additionalHeaders, cancellationToken: cancellationToken);
}

/// <summary>
Expand All @@ -117,6 +115,15 @@ public Task DeleteAsync(string conversationId, string id, Dictionary<string, str
return SendCoreAsync(conversationId, activity, isTargeted: true, additionalHeaders, cancellationToken);
}

/// <summary>
/// Reply to an existing activity with a targeted activity visible only to the specified recipient.
/// </summary>
public Task<SendActivityResponse?> ReplyTargetedAsync(string conversationId, string id, TeamsActivityInput activity, Dictionary<string, string>? additionalHeaders = null, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(activity);
return _client.ReplyToActivityAsync(conversationId, id, activity, _serviceUrl, isTargeted: true, requestContext: AgenticContext, customHeaders: additionalHeaders, cancellationToken: cancellationToken);
}

/// <summary>
/// Update an existing targeted activity in a conversation.
/// </summary>
Expand Down
12 changes: 10 additions & 2 deletions src/Microsoft.Teams.Apps/Clients/ConversationApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,7 @@ public Task<UpdateActivityResponse> UpdateActivityAsync(string conversationId, s
public Task<SendActivityResponse?> ReplyToActivityAsync(string conversationId, string id, TeamsActivityInput activity, Dictionary<string, string>? additionalHeaders = null, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(activity);
activity.ReplyToId = id;
return _client.SendActivityAsync(conversationId, activity, _serviceUrl, isTargeted: false, requestContext: AgenticContext, customHeaders: additionalHeaders, cancellationToken: cancellationToken);
return _client.ReplyToActivityAsync(conversationId, id, activity, _serviceUrl, isTargeted: false, requestContext: AgenticContext, customHeaders: additionalHeaders, cancellationToken: cancellationToken);
}

/// <summary>
Expand All @@ -109,6 +108,15 @@ public Task DeleteActivityAsync(string conversationId, string id, Dictionary<str
return _client.SendActivityAsync(conversationId, activity, _serviceUrl, isTargeted: true, requestContext: AgenticContext, customHeaders: additionalHeaders, cancellationToken: cancellationToken);
}

/// <summary>
/// Reply to an existing activity with a targeted activity visible only to the specified recipient.
/// </summary>
public Task<SendActivityResponse?> ReplyToTargetedActivityAsync(string conversationId, string id, TeamsActivityInput activity, Dictionary<string, string>? additionalHeaders = null, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(activity);
return _client.ReplyToActivityAsync(conversationId, id, activity, _serviceUrl, isTargeted: true, requestContext: AgenticContext, customHeaders: additionalHeaders, cancellationToken: cancellationToken);
}

/// <summary>
/// Update an existing targeted activity in a conversation.
/// </summary>
Expand Down
Loading
Loading