Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sample Code Optimization - Bot SSO Adaptive Card C# Sample. #1522

Merged
merged 4 commits into from
Jan 31, 2025
Merged
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
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
Loading