From d864d834cc121b938878c9bc4ea41327896e44da Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Mon, 4 May 2026 12:49:29 -0700 Subject: [PATCH 1/3] Improve user feedback and add suggested actions Enhanced messages with user names for sign-in, sign-out, status, profile, and calendar commands. Added suggested action buttons for common commands in sign-in and help responses. Updated OAuthFlow recipient logic for conversation type and clarified ConversationType XML docs. --- core/samples/SsoBot/Program.cs | 38 ++++++++++++++----- .../Microsoft.Teams.Apps/OAuth/OAuthFlow.cs | 4 +- .../Schema/TeamsConversation.cs | 2 +- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/core/samples/SsoBot/Program.cs b/core/samples/SsoBot/Program.cs index 2672fd5a6..880170645 100644 --- a/core/samples/SsoBot/Program.cs +++ b/core/samples/SsoBot/Program.cs @@ -38,13 +38,22 @@ auth.OnSignInComplete(async (context, tokenResponse, ct) => { - await context.SendActivityAsync("You're now signed in! Try `profile` or `calendar`.", ct); + await context.SendActivityAsync(new MessageActivity("You're now signed in! Try `profile` or `calendar`.") + .WithSuggestedActions( + new SuggestedActions() + { + Actions = new List() + { + new SuggestedAction() { Title = "Profile", Type = "imBack", Value = "profile" }, + new SuggestedAction() { Title = "Calendar", Type = "imBack", Value = "calendar" } + } + }), ct); }); auth.OnSignInFailure(async (context, failure, ct) => { string message = failure is not null - ? $"Sign-in failed: {failure.Code} — {failure.Message}" + ? $"User {context.Activity.From?.Name} Sign-in failed: {failure.Code} — {failure.Message}" : "Sign-in failed. Please try again."; await context.SendActivityAsync(message, ct); }); @@ -57,7 +66,7 @@ string? token = await context.SignIn(cancellationToken: ct); if (token is not null) { - await context.SendActivityAsync("You're already signed in.", ct); + await context.SendActivityAsync($"{context.Activity.From?.Name} You're already signed in.", ct); } // else: OAuthCard sent, SSO flow in progress -- OnSignInComplete will fire }); @@ -75,11 +84,11 @@ { string json = await http.GetStringAsync("https://graph.microsoft.com/v1.0/me", ct); string indentedJson = JsonSerializer.Serialize(JsonSerializer.Deserialize(json), new JsonSerializerOptions { WriteIndented = true }); - await context.SendActivityAsync(new MessageActivity($" ## Graph Me \n ```json\n{indentedJson}\n```") { TextFormat = TextFormats.Markdown }, ct); + await context.SendActivityAsync(new MessageActivity($" ## Graph Me [{context.Activity.From?.Name}] \n ```json\n{indentedJson}\n```") { TextFormat = TextFormats.Markdown }, ct); } catch (HttpRequestException ex) { - await context.SendActivityAsync($"Graph call failed: {ex.Message}", ct); + await context.SendActivityAsync($"[{context.Activity.From?.Name}] Graph call failed: {ex.Message}", ct); } }); @@ -96,24 +105,24 @@ string json = await http.GetStringAsync( "https://graph.microsoft.com/v1.0/me/events?$top=3&$select=subject,start,end&$orderby=start/dateTime", ct); string indentedJson = JsonSerializer.Serialize(JsonSerializer.Deserialize(json), new JsonSerializerOptions { WriteIndented = true }); - await context.SendActivityAsync(new MessageActivity($" ## Graph Calendar \n ```json\n{indentedJson}\n```") { TextFormat = TextFormats.Markdown }, ct); + await context.SendActivityAsync(new MessageActivity($" ## Graph Calendar [{context.Activity.From?.Name}] \n ```json\n{indentedJson}\n```") { TextFormat = TextFormats.Markdown }, ct); } catch (HttpRequestException ex) { - await context.SendActivityAsync($"Graph call failed: {ex.Message}", ct); + await context.SendActivityAsync($"[{context.Activity.From?.Name}] Graph call failed: {ex.Message}", ct); } }); bot.OnMessage("(?i)^logout$", async (context, ct) => { await context.SignOut(cancellationToken: ct); - await context.SendActivityAsync("Signed out.", ct); + await context.SendActivityAsync($"User {context.Activity.From?.Name} signed out.", ct); }); bot.OnMessage("(?i)^status$", async (context, ct) => { bool signedIn = await context.IsSignedInAsync(cancellationToken: ct); - await context.SendActivityAsync(signedIn ? "Signed in." : "Not signed in.", ct); + await context.SendActivityAsync(signedIn ? $"User {context.Activity.From?.Name} is signed in." : $"User {context.Activity.From?.Name} is not signed in.", ct); }); bot.OnMessage("(?i)^help$", async (context, ct) => @@ -131,7 +140,16 @@ """; await context.SendActivityAsync( - new MessageActivity(helpText) { TextFormat = TextFormats.Markdown }, ct); + new MessageActivity(helpText) { TextFormat = TextFormats.Markdown } + .WithSuggestedActions( + new SuggestedActions() { + Actions = new List() + { + new SuggestedAction() { Title = "Login", Type = "imBack", Value = "login" }, + new SuggestedAction() { Title = "Logout", Type = "imBack", Value = "logout" }, + new SuggestedAction() { Title = "Status", Type = "imBack", Value = "status" }, + } + }), ct); }); // ==================== INSTALL HANDLER ==================== diff --git a/core/src/Microsoft.Teams.Apps/OAuth/OAuthFlow.cs b/core/src/Microsoft.Teams.Apps/OAuth/OAuthFlow.cs index 5e37d283c..4dda1d8b5 100644 --- a/core/src/Microsoft.Teams.Apps/OAuth/OAuthFlow.cs +++ b/core/src/Microsoft.Teams.Apps/OAuth/OAuthFlow.cs @@ -180,10 +180,10 @@ public OAuthFlow OnSignInFailure(SignInFailureHandler handler) TeamsActivity oauthActivity = TeamsActivity.CreateBuilder() .WithConversationReference(context.Activity) - .WithRecipient(context.Activity.From, false) + .WithRecipient(context.Activity.From, context.Activity?.Conversation?.ConversationType != ConversationType.Personal) .WithAttachment(attachment) .Build(); - + await context.SendActivityAsync(oauthActivity, cancellationToken).ConfigureAwait(false); // Track that this user has a pending sign-in for this flow diff --git a/core/src/Microsoft.Teams.Apps/Schema/TeamsConversation.cs b/core/src/Microsoft.Teams.Apps/Schema/TeamsConversation.cs index 48acbbd3c..bc9f91de2 100644 --- a/core/src/Microsoft.Teams.Apps/Schema/TeamsConversation.cs +++ b/core/src/Microsoft.Teams.Apps/Schema/TeamsConversation.cs @@ -78,7 +78,7 @@ public TeamsConversation() [JsonPropertyName("tenantId")] public string? TenantId { get; set; } /// - /// Conversation Type. See for known values. + /// Conversation Type. See for known values. /// [JsonPropertyName("conversationType")] public string? ConversationType { get; set; } From 0a98c5f2732217b55230088bf9ad8a24f4d1b104 Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Tue, 5 May 2026 11:05:28 -0700 Subject: [PATCH 2/3] Improve bot message targeting for 1:1 and group chats Refactor bot responses to use MessageActivity with Markdown formatting, explicitly setting Recipient and IsTargeted properties. This ensures correct message delivery and formatting in both 1:1 and group chat contexts. Update OAuthFlow to set recipient for OAuth cards based on conversation type. --- core/samples/SsoBot/Program.cs | 41 ++++++++++++++++--- .../Microsoft.Teams.Apps/OAuth/OAuthFlow.cs | 2 +- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/core/samples/SsoBot/Program.cs b/core/samples/SsoBot/Program.cs index 880170645..6115fd565 100644 --- a/core/samples/SsoBot/Program.cs +++ b/core/samples/SsoBot/Program.cs @@ -55,7 +55,13 @@ await context.SendActivityAsync(new MessageActivity("You're now signed in! Try ` string message = failure is not null ? $"User {context.Activity.From?.Name} Sign-in failed: {failure.Code} — {failure.Message}" : "Sign-in failed. Please try again."; - await context.SendActivityAsync(message, ct); + var signInFailureMessage = new MessageActivity(message) + { + TextFormat = TextFormats.Markdown + }; + signInFailureMessage.Recipient = context.Activity.From; + signInFailureMessage.Recipient?.IsTargeted = context.Activity?.Conversation?.ConversationType == ConversationType.GroupChat; // only set IsTargeted for 1:1 chats to avoid issues in group contexts + await context.SendActivityAsync(signInFailureMessage, ct); }); // ==================== MESSAGE HANDLERS ==================== @@ -66,7 +72,13 @@ await context.SendActivityAsync(new MessageActivity("You're now signed in! Try ` string? token = await context.SignIn(cancellationToken: ct); if (token is not null) { - await context.SendActivityAsync($"{context.Activity.From?.Name} You're already signed in.", ct); + var alreadySignedInMessage = new MessageActivity($"You're already signed in, {context.Activity.From?.Name}!") + { + TextFormat = TextFormats.Markdown + }; + alreadySignedInMessage.Recipient = context.Activity.From; + alreadySignedInMessage.Recipient?.IsTargeted = context.Activity?.Conversation?.ConversationType == ConversationType.GroupChat; // only set IsTargeted for 1:1 chats to avoid issues in group contexts + await context.SendActivityAsync(alreadySignedInMessage, ct); } // else: OAuthCard sent, SSO flow in progress -- OnSignInComplete will fire }); @@ -84,7 +96,12 @@ await context.SendActivityAsync(new MessageActivity("You're now signed in! Try ` { string json = await http.GetStringAsync("https://graph.microsoft.com/v1.0/me", ct); string indentedJson = JsonSerializer.Serialize(JsonSerializer.Deserialize(json), new JsonSerializerOptions { WriteIndented = true }); - await context.SendActivityAsync(new MessageActivity($" ## Graph Me [{context.Activity.From?.Name}] \n ```json\n{indentedJson}\n```") { TextFormat = TextFormats.Markdown }, ct); + + var msgResponse = new MessageActivity($" ## Graph Me [{context.Activity.From?.Name}] \n ```json\n{indentedJson}\n```") + { TextFormat = TextFormats.Markdown }; + msgResponse.Recipient = context.Activity.From; + msgResponse.Recipient?.IsTargeted = context.Activity?.Conversation?.ConversationType == ConversationType.GroupChat; // only set IsTargeted for 1:1 chats to avoid issues in group contexts + await context.SendActivityAsync(msgResponse, ct); } catch (HttpRequestException ex) { @@ -116,13 +133,27 @@ await context.SendActivityAsync(new MessageActivity("You're now signed in! Try ` bot.OnMessage("(?i)^logout$", async (context, ct) => { await context.SignOut(cancellationToken: ct); - await context.SendActivityAsync($"User {context.Activity.From?.Name} signed out.", ct); + var signOutMessage = new MessageActivity($"You've been signed out, {context.Activity.From?.Name}.") + { + TextFormat = TextFormats.Markdown + }; + signOutMessage.Recipient = context.Activity.From; + signOutMessage.Recipient?.IsTargeted = context.Activity?.Conversation?.ConversationType == ConversationType.GroupChat; // only set IsTargeted for 1:1 chats to avoid issues in group contexts + await context.SendActivityAsync(signOutMessage, ct); }); bot.OnMessage("(?i)^status$", async (context, ct) => { bool signedIn = await context.IsSignedInAsync(cancellationToken: ct); - await context.SendActivityAsync(signedIn ? $"User {context.Activity.From?.Name} is signed in." : $"User {context.Activity.From?.Name} is not signed in.", ct); + var signInStatusMessage = new MessageActivity(signedIn + ? $"User {context.Activity.From?.Name} is signed in." + : $"User {context.Activity.From?.Name} is not signed in.") + { + TextFormat = TextFormats.Markdown + }; + signInStatusMessage.Recipient = context.Activity.From; + signInStatusMessage.Recipient?.IsTargeted = context.Activity?.Conversation?.ConversationType == ConversationType.GroupChat; // only set IsTargeted for 1:1 chats to avoid issues in group contexts + await context.SendActivityAsync(signInStatusMessage, ct); }); bot.OnMessage("(?i)^help$", async (context, ct) => diff --git a/core/src/Microsoft.Teams.Apps/OAuth/OAuthFlow.cs b/core/src/Microsoft.Teams.Apps/OAuth/OAuthFlow.cs index 4dda1d8b5..0a26312f0 100644 --- a/core/src/Microsoft.Teams.Apps/OAuth/OAuthFlow.cs +++ b/core/src/Microsoft.Teams.Apps/OAuth/OAuthFlow.cs @@ -180,7 +180,7 @@ public OAuthFlow OnSignInFailure(SignInFailureHandler handler) TeamsActivity oauthActivity = TeamsActivity.CreateBuilder() .WithConversationReference(context.Activity) - .WithRecipient(context.Activity.From, context.Activity?.Conversation?.ConversationType != ConversationType.Personal) + .WithRecipient(context.Activity.From, context.Activity?.Conversation?.ConversationType == ConversationType.GroupChat) .WithAttachment(attachment) .Build(); From ee4e319bf775e811e82540ec46bf66c03b91276b Mon Sep 17 00:00:00 2001 From: "Ricardo Minguez Pablos (RIDO)" Date: Wed, 27 May 2026 09:24:02 -0700 Subject: [PATCH 3/3] Suppress ExperimentalTeamsTargeted warning in SsoBot.csproj Added the ExperimentalTeamsTargeted warning to the property in SsoBot.csproj to prevent build warnings related to experimental Teams features. No other changes were made. --- core/samples/SsoBot/SsoBot.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/core/samples/SsoBot/SsoBot.csproj b/core/samples/SsoBot/SsoBot.csproj index 0f379b438..8421911b7 100644 --- a/core/samples/SsoBot/SsoBot.csproj +++ b/core/samples/SsoBot/SsoBot.csproj @@ -4,6 +4,7 @@ net10.0 enable enable + $(NoWarn);ExperimentalTeamsTargeted