Skip to content

Commit

Permalink
Merge pull request #1522 from OfficeDev/v-hrajandira/Sample_Code_Opti…
Browse files Browse the repository at this point in the history
…mization

Sample Code Optimization - Bot SSO Adaptive Card C# Sample.
  • Loading branch information
Pawank-MSFT authored Jan 31, 2025
2 parents a052c83 + 5f697da commit d345fdf
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 140 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,22 @@ namespace Microsoft.BotBuilderSamples
{
public class AdapterWithErrorHandler : CloudAdapter
{
public AdapterWithErrorHandler(BotFrameworkAuthentication auth, ILogger<IBotFrameworkHttpAdapter> logger, ConversationState conversationState = default)
// Constructor that initializes the bot framework authentication and logger.
public AdapterWithErrorHandler(BotFrameworkAuthentication auth, ILogger<IBotFrameworkHttpAdapter> logger)
: base(auth, logger)
{
// Define the error handling behavior during the bot's turn.
OnTurnError = async (turnContext, exception) =>
{
// Log any leaked exception from the application.
// NOTE: In production environment, you should consider logging this to
// Azure Application Insights. Visit https://aka.ms/bottelemetry to see how
// to add telemetry capture to your bot.
logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}");
// Log the exception details for debugging and tracking errors.
logger.LogError(exception, $"[OnTurnError] unhandled error: {exception.Message}");

// Uncomment below commented line for local debugging.
// For development purposes, uncomment to provide a custom error message to users locally.
// await turnContext.SendActivityAsync($"Sorry, it looks like something went wrong. Exception Caught: {exception.Message}");

// Send a trace activity, which will be displayed in the Bot Framework Emulator
// Send a trace activity to the Bot Framework Emulator for deeper debugging.
await turnContext.TraceActivityAsync("OnTurnError Trace", exception.Message, "https://www.botframework.com/schemas/error", "TurnError");
};
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ namespace Microsoft.BotBuilderSamples
// and the requirement is that all BotState objects are saved at the end of a turn.
public class DialogBot<T> : TeamsActivityHandler where T : Dialog
{
protected readonly BotState _conversationState;
protected readonly Dialog _dialog;
protected readonly ILogger _logger;
protected readonly BotState _userState;
protected string _connectionName { get; }
protected readonly BotState _conversationState; // Represents the conversation state
protected readonly Dialog _dialog; // The dialog logic to run
protected readonly ILogger _logger; // Logger for debugging and tracing
protected readonly BotState _userState; // Represents the user state
protected string _connectionName { get; } // Connection name for OAuth

// Constructor to initialize the bot with necessary dependencies
public DialogBot(ConversationState conversationState, UserState userState, T dialog, ILogger<DialogBot<T>> logger, string connectionName)
{
_conversationState = conversationState;
Expand All @@ -49,6 +50,7 @@ public DialogBot(ConversationState conversationState, UserState userState, T dia
/// <param name="turnContext">The context for the current turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
// Get the sign-in link for OAuth
private async Task<string> GetSignInLinkAsync(ITurnContext turnContext, CancellationToken cancellationToken)
{
var userTokenClient = turnContext.TurnState.Get<UserTokenClient>();
Expand All @@ -62,13 +64,16 @@ private async Task<string> GetSignInLinkAsync(ITurnContext turnContext, Cancella
/// <param name="turnContext">The context for the current turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
// OnTurnAsync: Handles parallel saving of conversation and user state changes
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default(CancellationToken))
{
await base.OnTurnAsync(turnContext, cancellationToken);

// Save any state changes that might have occurred during the turn.
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
await _userState.SaveChangesAsync(turnContext, false, cancellationToken);
// Save any state changes in parallel to improve performance
await Task.WhenAll(
_conversationState.SaveChangesAsync(turnContext, false, cancellationToken),
_userState.SaveChangesAsync(turnContext, false, cancellationToken)
);
}

/// <summary>
Expand All @@ -77,22 +82,28 @@ private async Task<string> GetSignInLinkAsync(ITurnContext turnContext, Cancella
/// <param name="turnContext">The context for the current turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
// Simplified message activity handling to trigger appropriate adaptive card based on the message command
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
var signInLink = await GetSignInLinkAsync(turnContext, cancellationToken).ConfigureAwait(false);
if (turnContext.Activity.Text.Contains("login"))
await HandleCommandAsync(turnContext.Activity.Text, turnContext, signInLink, cancellationToken);
}

// Helper function to handle commands and send the appropriate adaptive card
private async Task HandleCommandAsync(string command, ITurnContext<IMessageActivity> turnContext, string signInLink, CancellationToken cancellationToken)
{
var commandToFileMap = new Dictionary<string, string>
{
string[] path = { ".", "Resources", "options.json" };
var member = await TeamsInfo.GetMemberAsync(turnContext, turnContext.Activity.From.Id, cancellationToken);
var initialAdaptiveCard = GetAdaptiveCardFromFileName(path, signInLink, turnContext.Activity.From.Name, member.Id);
await turnContext.SendActivityAsync(MessageFactory.Attachment(initialAdaptiveCard), cancellationToken);
}
else if (turnContext.Activity.Text.Contains("PerformSSO"))
{ "login", "options.json" },
{ "PerformSSO", "AdaptiveCardWithSSOInRefresh.json" }
};

if (commandToFileMap.ContainsKey(command))
{
string[] path = { ".", "Resources", "AdaptiveCardWithSSOInRefresh.json" };
string[] path = { ".", "Resources", commandToFileMap[command] };
var member = await TeamsInfo.GetMemberAsync(turnContext, turnContext.Activity.From.Id, cancellationToken);
var initialAdaptiveCard = GetAdaptiveCardFromFileName(path, signInLink, turnContext.Activity.From.Name, member.Id);
await turnContext.SendActivityAsync(MessageFactory.Attachment(initialAdaptiveCard), cancellationToken);
var adaptiveCard = GetAdaptiveCardFromFileName(path, signInLink, turnContext.Activity.From.Name, member.Id);
await turnContext.SendActivityAsync(MessageFactory.Attachment(adaptiveCard), cancellationToken);
}
else
{
Expand All @@ -106,6 +117,7 @@ protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivi
/// <param name="turnContext">The context for the current turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
// Override to handle invoke activities, such as OAuth and adaptive card actions
protected override async Task<InvokeResponse> OnInvokeActivityAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
{
if (turnContext.Activity.Name == "adaptiveCard/action")
Expand All @@ -118,40 +130,38 @@ protected override async Task<InvokeResponse> OnInvokeActivityAsync(ITurnContext
if (value["action"] == null)
return null;

JObject actiondata = JsonConvert.DeserializeObject<JObject>(value["action"].ToString());
JObject actionData = JsonConvert.DeserializeObject<JObject>(value["action"].ToString());

if (actiondata["verb"] == null)
if (actionData["verb"] == null)
return null;
string verb = actiondata["verb"].ToString();

string verb = actionData["verb"].ToString();

JObject authentication = null;
string state = null;

// When adaptiveCard/action invoke activity from teams contains token in response to sso flow from earlier invoke.
// Check for authentication token or state
if (value["authentication"] != null)
{
authentication = JsonConvert.DeserializeObject<JObject>(value["authentication"].ToString());
}

// When adaptiveCard/action invoke activity from teams contains 6 digit state in response to nominal sign in flow from bot.
string state = null;
if (value["state"] != null)
{
state = value["state"].ToString();
}

// authToken and state are absent, handle verb
// Token and state are absent, initiate SSO
if (authentication == null && state == null)
{
switch (verb)
{ // when token is absent in the invoke. We can initiate SSO in response to the invoke
case "initiateSSO":
return await initiateSSOAsync(turnContext, cancellationToken);
if (verb == "initiateSSO")
{
return await InitiateSSOAsync(turnContext, cancellationToken);
}
}
else
{
return createAdaptiveCardInvokeResponseAsync(authentication, state);
return CreateAdaptiveCardInvokeResponseAsync(authentication, state);
}
}

Expand All @@ -169,36 +179,26 @@ protected override async Task<InvokeResponse> OnInvokeActivityAsync(ITurnContext
/// <param name="isBasicRefresh">Refresh type</param>
/// <param name="fileName">AdaptiveCardResponse.json</param>
/// <returns>A task that represents the work queued to execute.</returns>
private InvokeResponse createAdaptiveCardInvokeResponseAsync(JObject authentication, string state, bool isBasicRefresh = false, string fileName = "AdaptiveCardResponse.json")
private InvokeResponse CreateAdaptiveCardInvokeResponseAsync(JObject authentication, string state, bool isBasicRefresh = false, string fileName = "AdaptiveCardResponse.json")
{
// Verify token is present or not.

bool isTokenPresent = authentication != null ? true : false;
bool isStatePresent = state != null && state != "" ? true : false;

string[] filepath = { ".", "Resources", fileName };
string authResultData = (authentication != null) ? "SSO success" : (state != null && state != "") ? "OAuth success" : "SSO/OAuth failed";

var adaptiveCardJson = File.ReadAllText(Path.Combine(filepath));
AdaptiveCardTemplate template = new AdaptiveCardTemplate(adaptiveCardJson);
var authResultData = isTokenPresent ? "SSO success" : isStatePresent ? "OAuth success" : "SSO/OAuth failed";

if (isBasicRefresh)
{
authResultData = "Refresh done";
}

var payloadData = new
{
authResult = authResultData,
};

var cardJsonstring = template.Expand(payloadData);
string[] filePath = { ".", "Resources", fileName };
var adaptiveCardJson = File.ReadAllText(Path.Combine(filePath));
AdaptiveCardTemplate template = new AdaptiveCardTemplate(adaptiveCardJson);
var payloadData = new { authResult = authResultData };
var cardJsonString = template.Expand(payloadData);

var adaptiveCardResponse = new AdaptiveCardInvokeResponse()
{
StatusCode = 200,
Type = AdaptiveCard.ContentType,
Value = JsonConvert.DeserializeObject(cardJsonstring)
Value = JsonConvert.DeserializeObject(cardJsonString)
};

return CreateInvokeResponse(adaptiveCardResponse);
Expand All @@ -210,12 +210,12 @@ private InvokeResponse createAdaptiveCardInvokeResponseAsync(JObject authenticat
/// <param name="turnContext">The context for the current turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
private async Task<InvokeResponse> initiateSSOAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
private async Task<InvokeResponse> InitiateSSOAsync(ITurnContext<IInvokeActivity> turnContext, CancellationToken cancellationToken)
{
var signInLink = await GetSignInLinkAsync(turnContext, cancellationToken).ConfigureAwait(false);
var oAuthCard = new OAuthCard
{
Text = "Signin Text",
Text = "Please sign in",
ConnectionName = _connectionName,
TokenExchangeResource = new TokenExchangeResource
{
Expand All @@ -227,7 +227,7 @@ private async Task<InvokeResponse> initiateSSOAsync(ITurnContext<IInvokeActivity
{
Type = ActionTypes.Signin,
Value = signInLink,
Title = "Please sign in",
Title = "Sign In",
},
}
};
Expand All @@ -250,6 +250,7 @@ private async Task<InvokeResponse> initiateSSOAsync(ITurnContext<IInvokeActivity
/// <param name="name">createdBy</param>
/// <param name="userMRI">createdById</param>
/// <returns></returns>
// Method to retrieve adaptive card from a file and expand with dynamic data
private Attachment GetAdaptiveCardFromFileName(string[] filepath, string signInLink, string name = null, string userMRI = null)
{
var adaptiveCardJson = File.ReadAllText(Path.Combine(filepath));
Expand All @@ -259,9 +260,9 @@ private Attachment GetAdaptiveCardFromFileName(string[] filepath, string signInL
createdById = userMRI,
createdBy = name
};
var cardJsonstring = template.Expand(payloadData);
var card = JsonConvert.DeserializeObject<JObject>(cardJsonstring);

var cardJsonString = template.Expand(payloadData);
var card = JsonConvert.DeserializeObject<JObject>(cardJsonString);
var adaptiveCardAttachment = new Attachment()
{
ContentType = "application/vnd.microsoft.card.adaptive",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,36 @@ namespace Microsoft.BotBuilderSamples
// This bot is derived (view DialogBot<T>) from the TeamsActivityHandler class currently included as part of this sample.
public class TeamsBot : DialogBot<MainDialog>
{
// Constructor to initialize the bot with necessary dependencies
public TeamsBot(ConversationState conversationState, UserState userState, MainDialog dialog, ILogger<DialogBot<MainDialog>> logger, IConfiguration configuration)
: base(conversationState, userState, dialog, logger, configuration["ConnectionName"])
{
// Check if the ConnectionName exists in the configuration
if (string.IsNullOrEmpty(configuration["ConnectionName"]))
{
logger.LogError("ConnectionName is missing from configuration.");
}
}

/// <summary>
/// Override this in a derived class to provide logic for when members other than the bot join the conversation, such as your bot's welcome logic.
/// Override this in a derived class to provide logic for when members, except the bot, join the conversation, such as your bot's welcome logic.
/// </summary>
/// <param name="membersAdded">A list of all the members added to the conversation, as described by the conversation update activity.</param>
/// <param name="turnContext">A strongly-typed context object for this turn.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <returns>A task that represents the work queued to execute.</returns>
protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
foreach (var member in turnContext.Activity.MembersAdded)
// Iterate over all members added to the conversation.
foreach (var member in membersAdded)
{
// Ensure that the bot doesn't greet itself
if (member.Id != turnContext.Activity.Recipient.Id)
{
await turnContext.SendActivityAsync(MessageFactory.Text("Welcome to Universal Adaptive Cards. Type 'login' to get sign in universal sso."), cancellationToken);
// Send a welcome message to new members.
await turnContext.SendActivityAsync(MessageFactory.Text("Welcome to Universal Adaptive Cards. Type 'login' to sign in using Universal SSO."), cancellationToken);
}
}
}
}
}
}
Loading

0 comments on commit d345fdf

Please sign in to comment.