Microsoft Teams bot - link unfurling auth flow - c#

Can't find good example of auth flow for link unfurling. I managed to run oauth flow using this example. But after user provided login and password and bot hits OnTeamsAppBasedLinkQueryAsync second time GetUserTokenAsync still returns null. So I don't follow where should I get then token from when auth flow is finished. Should I persist it somehow? Will Teams send me the token on every request or how it should work?
So in my case following code always returns null:
var tokenResponse = await (turnContext.Adapter as IUserTokenProvider)
.GetUserTokenAsync(turnContext, _connectionName, default(string),
cancellationToken: cancellationToken);

It seems the 'state' field is not present on AppBasedLinkQuery. When the auth flow completes, OnTeamsAppBasedLinkQueryAsync will be called again and the turnContext.Activity.Value will contain the url and the 'state' (or magic code). We will get this field added to AppBasedLinkQuery (created an issue here: microsoft/botbuilder-dotnet#3429 ).
A workaround is to retrieve the state/magiccode directly from the Activity.Value Something like:
protected async override Task<MessagingExtensionResponse> OnTeamsAppBasedLinkQueryAsync(ITurnContext<IInvokeActivity> turnContext, AppBasedLinkQuery query, CancellationToken cancellationToken)
{
var magicCode = string.Empty;
var state = (turnContext.Activity.Value as Newtonsoft.Json.Linq.JObject).Value<string>("state");
if (!string.IsNullOrEmpty(state))
{
int parsed = 0;
if (int.TryParse(state, out parsed))
{
magicCode = parsed.ToString();
}
}
var tokenResponse = await(turnContext.Adapter as IUserTokenProvider).GetUserTokenAsync(turnContext, _connectionName, magicCode, cancellationToken: cancellationToken);
if (tokenResponse == null || string.IsNullOrEmpty(tokenResponse.Token))
{
// There is no token, so the user has not signed in yet.
// Retrieve the OAuth Sign in Link to use in the MessagingExtensionResult Suggested Actions
var signInLink = await(turnContext.Adapter as IUserTokenProvider).GetOauthSignInLinkAsync(turnContext, _connectionName, cancellationToken);
return new MessagingExtensionResponse
{
ComposeExtension = new MessagingExtensionResult
{
Type = "auth",
SuggestedActions = new MessagingExtensionSuggestedAction
{
Actions = new List<CardAction>
{
new CardAction
{
Type = ActionTypes.OpenUrl,
Value = signInLink,
Title = "Bot Service OAuth",
},
},
},
},
};
}
var heroCard = new ThumbnailCard
{
Title = "Thumbnail Card",
Text = query.Url,
Images = new List<CardImage> { new CardImage("https://raw.githubusercontent.com/microsoft/botframework-sdk/master/icon.png") },
};
var attachments = new MessagingExtensionAttachment(HeroCard.ContentType, null, heroCard);
var result = new MessagingExtensionResult("list", "result", new[] { attachments });
return new MessagingExtensionResponse(result);
}

Related

V4 Bot Framework CreateConversationAsync (ConversationReference obsolete)

The code below was working - when a new user is added to the Teams channel the bot sends a welcome message to the user personally and not to the whole channel. For some reason it is no longer working - I believe it has to do with CreateConversationAsync() method. The V4 docs state: "This method is now obsolete because the ConversationReference argument is now redundant. Use the overload without this argument." but I haven't been able to figure out how to properly update the code below to work.
CreateConversationAsync: (This method passes the conversation reference (now obsolete) to ContinueConversationAsync())
ConversationReference conversationReference = null;
return await ((BotFrameworkAdapter)turnContext.Adapter).CreateConversationAsync(
teamsChannelId,
serviceUrl,
credentials,
conversationParameters,
async (t1, c1) =>
{
conversationReference = t1.Activity.GetConversationReference();
await Task.FromResult(false).ConfigureAwait(false);
}, cancellationToken).ContinueWith(_ => { return conversationReference; }).ConfigureAwait(false);
ContinueConversationAsync:
if (conversationReference != null)
{
await turnContext.Adapter.ContinueConversationAsync(
BotAppId,
conversationReference,
async (t2, c2) =>
{
await t2.SendActivityAsync(MessageFactory.Text(messages[0]), cancellationToken: c2).ConfigureAwait(false);
},cancellationToken).ConfigureAwait(false);
}
ConversationParameters for reference:
var conversationParameters = new ConversationParameters
{
IsGroup = false,
Bot = this.TeamsHelper.GetRecipient(turnContext),
Members = new List<ChannelAccount>() { member },
TenantId = this.TeamsHelper.GetChannelTennantId(channelData),
TopicName = "Testing",
};
Any help would be greatly appreciated!
------ UPDATED WITH SNIPPET ------
var teamsChannelId = turnContext.Activity.TeamsGetChannelId();
var serviceUrl = turnContext.Activity.ServiceUrl;
var credentials = new MicrosoftAppCredentials(BotAppId, BotAppPassword);
var channelData = turnContext.Activity.GetChannelData<TeamsChannelData>();
var conversationParameters = new ConversationParameters
{
IsGroup = false,
Bot = turnContext.Activity.Recipient,
Members = new List<ChannelAccount>() { member },
//TenantId = turnContext.Activity.Conversation.TenantId,
TenantId = channelData.Tenant.Id,
TopicName = "Testing Topic ",
};
var conversationReference = new ConversationReference()
{
ChannelId = teamsChannelId,
Bot = turnContext.Activity.Recipient,
ServiceUrl = serviceUrl,
Conversation = new ConversationAccount() { ConversationType = "channel", IsGroup = false, Id = teamsChannelId, TenantId = channelData.Tenant.Id },
};
await ((BotFrameworkAdapter)turnContext.Adapter).CreateConversationAsync(
teamsChannelId,
serviceUrl,
credentials,
conversationParameters,
async (t1, c1) =>
{
await ((BotFrameworkAdapter)turnContext.Adapter).ContinueConversationAsync(
BotAppId,
conversationReference,
async (t2, c2) =>
{
await t2.SendActivityAsync(MessageFactory.Text("This will be the first response to the new thread"), c2).ConfigureAwait(false);
},
cancellationToken).ConfigureAwait(false);
},
cancellationToken).ConfigureAwait(false);
Wanted to update everyone with a suggestion I was given:
If you want to send a message to user personally, you will need to pass conversation Id of that 1:1 chat not the channel, so to get that, conversation reference should be like this (please see variable conversationReference inside CreateConversationAsync) :
await ((BotFrameworkAdapter)turnContext.Adapter).CreateConversationAsync(
"msteams",
serviceUrl,
credentials,
conversationParameters,
async (t1, c1) =>
{
var userConversation = t1.Activity.Conversation.Id;
var conversationReference = new ConversationReference
{
ServiceUrl = serviceUrl,
Conversation = new ConversationAccount
{
Id = userConversation,
},
};
await ((BotFrameworkAdapter)turnContext.Adapter).ContinueConversationAsync(
BotAppId,
conversationReference,
async (t2, c2) =>
{
await t2.SendActivityAsync(MessageFactory.Text("This will be the first response to the new thread"), c2).ConfigureAwait(false);
},
cancellationToken).ConfigureAwait(false);
},
cancellationToken).ConfigureAwait(false);
I was able to test this approach locally, and it worked!

Power BI Embedded with Roles, allowing Row Level Security

I have implemented the Microsoft Example for Embed for your customers from Github, which works perfectly. Link
I am now extending it which there are articles showing using both V1 and V2 of the API, both result in the same error:
Operation returned an invalid status code 'BadRequest'
at
Microsoft.PowerBI.Api.ReportsOperations.GenerateTokenInGroupWithHttpMessagesAsync(Guid
groupId, Guid reportId, GenerateTokenRequest requestParameters,
Dictionary`2 customHeaders, CancellationToken cancellationToken)
[HttpGet]
public async Task<string> GetEmbedInfo()
{
try
{
// Validate whether all the required configurations are provided in appsettings.json
string configValidationResult = ConfigValidatorService.ValidateConfig(azureAd, powerBI);
if (configValidationResult != null)
{
HttpContext.Response.StatusCode = 400;
return configValidationResult;
}
EmbedParams embedParams = await pbiEmbedService.GetEmbedParams(new Guid(powerBI.Value.WorkspaceId), new Guid(powerBI.Value.ReportId));
//EmbedParams embedParams = await pbiEmbedService.GetEmbedToken4(new Guid(powerBI.Value.WorkspaceId), new Guid(powerBI.Value.ReportId));
return JsonSerializer.Serialize<EmbedParams>(embedParams);
}
catch (Exception ex)
{
HttpContext.Response.StatusCode = 500;
return ex.Message + "\n\n" + ex.StackTrace;
}
}
The above code is getting called and per the demo.
public async Task<EmbedParams> GetEmbedParams(Guid workspaceId, Guid reportId, [Optional] Guid additionalDatasetId)
{
PowerBIClient pbiClient = this.GetPowerBIClient();
// Get report info
var pbiReport = await pbiClient.Reports.GetReportInGroupAsync(workspaceId, reportId);
//var generateTokenRequestParameters = new GenerateTokenRequest("View", null, identities: new List<EffectiveIdentity> { new EffectiveIdentity(username: "**************", roles: new List<string> { "****", "****" }, datasets: new List<string> { "datasetId" }) });
//var tokenResponse = pbiClient.Reports.GenerateTokenInGroupAsync("groupId", "reportId", generateTokenRequestParameters);
// Create list of datasets
var datasetIds = new List<Guid>();
// Add dataset associated to the report
datasetIds.Add(Guid.Parse(pbiReport.DatasetId));
// Append additional dataset to the list to achieve dynamic binding later
if (additionalDatasetId != Guid.Empty)
{
datasetIds.Add(additionalDatasetId);
}
// Add report data for embedding
var embedReports = new List<EmbedReport>() {
new EmbedReport
{
ReportId = pbiReport.Id, ReportName = pbiReport.Name, EmbedUrl = pbiReport.EmbedUrl
}
};
// Get Embed token multiple resources
var embedToken = await GetEmbedToken4(workspaceId, reportId);
// Capture embed params
var embedParams = new EmbedParams
{
EmbedReport = embedReports,
Type = "Report",
EmbedToken = embedToken
};
return embedParams;
}
The above code is per the demo apart from one line, which is calling the next method:
var embedToken = await GetEmbedToken4(workspaceId, reportId);
public EmbedToken GetEmbedToken(Guid reportId, IList<Guid> datasetIds, [Optional] Guid targetWorkspaceId)
{
PowerBIClient pbiClient = this.GetPowerBIClient();
// Create a request for getting Embed token
// This method works only with new Power BI V2 workspace experience
var tokenRequest = new GenerateTokenRequestV2(
reports: new List<GenerateTokenRequestV2Report>() { new GenerateTokenRequestV2Report(reportId) },
datasets: datasetIds.Select(datasetId => new GenerateTokenRequestV2Dataset(datasetId.ToString())).ToList(),
targetWorkspaces: targetWorkspaceId != Guid.Empty ? new List<GenerateTokenRequestV2TargetWorkspace>() { new GenerateTokenRequestV2TargetWorkspace(targetWorkspaceId) } : null
);
// Generate Embed token
var embedToken = pbiClient.EmbedToken.GenerateToken(tokenRequest);
return embedToken;
}
The above code is per the example with no roles being passed in or EffectiveIdentity. This works.
public async Task<EmbedToken> GetEmbedToken4(Guid workspaceId, Guid reportId, string accessLevel = "view")
{
PowerBIClient pbiClient = this.GetPowerBIClient();
var pbiReport = pbiClient.Reports.GetReportInGroup(workspaceId, reportId);
string dataSet = pbiReport.DatasetId.ToString();
// Generate token request for RDL Report
var generateTokenRequestParameters = new GenerateTokenRequest(
accessLevel: accessLevel,
datasetId: dataSet,
identities: new List<EffectiveIdentity> { new EffectiveIdentity(username: "******", roles: new List<string> { "********" }) }
);
// Generate Embed token
var embedToken = pbiClient.Reports.GenerateTokenInGroup(workspaceId, reportId, generateTokenRequestParameters);
return embedToken;
}
This is the method to return the token with the roles and effective Identity. This results in the error, but no message or helpful feedback.
OK, after much research overnight the Bad Request response does hide an English message which is not show in the browser. The debugger doesn't have the symbols for the part that causes the error, but I found it by using Fiddler proxy when the actual API responded to the request. In my case, if you send an ID to enable RLS, but the version of the report on the server doesn't have it, this doesn't ignore it, it refuses to give a token to anything. From reading many posts, the Bad Request is just a poor error message when the actual response from the API itself (not the package or the example code that the sample uses with it presents). Hope this helps someone in the future.

Microsoft Graph - Create outlook/teams meeting as a application .Net/C#

I'm trying to create a meeting as a application, add attendees, determine time availability.
This is what I have so far :
Auth
private async Task<ClientCredentialProvider> GetToken()
{
var confidentialClientApplication = ConfidentialClientApplicationBuilder
.Create(_microsoftAppId)
.WithTenantId(_microsoftTenantId)
.WithClientSecret(_microsoftAppPassword)
.Build();
ClientCredentialProvider authProvider = new ClientCredentialProvider(confidentialClientApplication);
return authProvider;
}
Meeting request
public async Task GetMeetingtime()
{
var authProvider = await GetToken();
GraphServiceClient graphClient = new GraphServiceClient(authProvider);
var attendees = new List<AttendeeBase>()
{
new AttendeeBase
{
Type = AttendeeType.Required,
EmailAddress = new EmailAddress
{
Name = "john doe",
Address = "john.doe#onmicrosoft.com"
}
},
new AttendeeBase
{
Type = AttendeeType.Required,
EmailAddress = new EmailAddress
{
Name = "john toe",
Address = "john.toe#onmicrosoft.com"
}
}
};
var locationConstraint = new LocationConstraint
{
IsRequired = false,
SuggestLocation = false,
Locations = new List<LocationConstraintItem>()
{
new LocationConstraintItem
{
ResolveAvailability = false,
DisplayName = "Conf room Hood"
}
}
};
var timeConstraint = new TimeConstraint
{
ActivityDomain = ActivityDomain.Work,
TimeSlots = new List<TimeSlot>()
{
new TimeSlot
{
Start = new DateTimeTimeZone
{
DateTime = "2020-12-10T09:00:00",
TimeZone = "Pacific Standard Time"
},
End = new DateTimeTimeZone
{
DateTime = "2020-12-10T17:00:00",
TimeZone = "Pacific Standard Time"
}
}
}
};
var isOrganizerOptional = false;
var meetingDuration = new Duration("PT1H");
var returnSuggestionReasons = true;
var minimumAttendeePercentage = (double)100;
await graphClient
.Me
.FindMeetingTimes(attendees, locationConstraint, timeConstraint, meetingDuration, null, isOrganizerOptional, returnSuggestionReasons, minimumAttendeePercentage)
.Request()
.Header("Prefer", "outlook.timezone=\"Pacific Standard Time\"")
.PostAsync();
}
This is the error I get:
Current authenticated context is not valid for this request. This occurs when a request is made to an endpoint that requires user sign-in. For example, /me requires a signed-in user. Acquire a token on behalf of a user to make requests to these endpoints. Use the OAuth 2.0 authorization code flow for mobile and native apps and the OAuth 2.0 implicit flow for single-page web apps.
How can I solve this?
According to docs, the findMeetingTimes API does not support application-only access: https://learn.microsoft.com/en-us/graph/api/user-findmeetingtimes?view=graph-rest-1.0&tabs=http.
It can only be called in the context of a signed-in user.
Now, you could try to use .Users["user-id"] instead of .Me just in case the docs are wrong about this.
"me" only makes sense when calling the API on behalf of a user, which you are not.
Calendar events can be created as an application that has write access to the users' calendars.

Alexa.NET cannot create a reminder : Invalid Bearer Token

I want to create push notifications to my Alexa Devide. Due the push notification program is closed I am trying to create reminders. The final idea is to create an Azure Function with this code and being called when a TFS Build faild.
I'm using Alexa.NET and Alexa.NET.Reminders from a console application, already have and Alexa Skill with all the permissions granted, in the Alexa portal and in the mobile app.
Everything seems to work nice until I try to read the reminders in my account, when get an exception "Invalid Bearer Token"
this is the code:
[Fact]
public async Task SendNotificationTest()
{
var clientId = "xxxx";
var clientSecret = "yyyy";
var alexaClient = clientId;
var alexaSecret = clientSecret;
var accessToken = new Alexa.NET.AccessTokenClient(Alexa.NET.AccessTokenClient.ApiDomainBaseAddress);
var token = await accessToken.Send(alexaClient, alexaSecret);
var reminder = new Reminder
{
RequestTime = DateTime.UtcNow,
Trigger = new RelativeTrigger(12 * 60 * 60),
AlertInformation = new AlertInformation(new[] { new SpokenContent("test", "en-GB") }),
PushNotification = PushNotification.Disabled
};
var total = JsonConvert.SerializeObject(reminder);
var client = new RemindersClient("https://api.eu.amazonalexa.com", token.Token);
var alertList = await client.Get();
foreach (var alertInformation in alertList.Alerts)
{
Console.WriteLine(alertInformation.ToString());
}
try
{
var response = await client.Create(reminder);
}
catch (Exception ex)
{
var x = ex.Message;
}
}
Are there any examples to get the access token?
Am I missing a step in the process?
Thanks in advance.
N.B. The reminders client requires that you have a skill with reminders persmission enabled, and the user must have given your skill reminders permission (even if its your development account)
Creating a reminder
using Alexa.NET.Response
using Alexa.NET.Reminders
....
var reminder = new Reminder
{
RequestTime = DateTime.UtcNow,
Trigger = new RelativeTrigger(12 * 60 * 60),
AlertInformation = new AlertInformation(new[] { new SpokenContent("it's a test", "en-GB") }),
PushNotification = PushNotification.Disabled
};
var client = new RemindersClient(skillRequest);
var alertDetail = await client.Create(reminder);
Console.WriteLine(alertDetail.AlertToken);
Retrieving Current Reminders
// Single reminders can be retrieved with client.Get(alertToken)
var alertList = await client.Get();
foreach(var alertInformation in alertList.Alerts)
{
//Your logic here
}
Deleting a Reminder
await client.Delete(alertToken);

Making DialogFlow v2 DetectIntent Calls w/ C# (including input context)

So I finally figured out a way to successfully make detect intent calls and provide an input context. My question is whether or not this is the CORRECT (or best) way to do it:
(And yes, I know you can just call DetectIntent(agent, session, query) but I have to provide a input context(s) depending on the request)
var query = new QueryInput
{
Text = new TextInput
{
Text = model.Content,
LanguageCode = string.IsNullOrEmpty(model.Language) ? "en-us" : model.Language,
}
};
var commonContext = new global::Google.Cloud.Dialogflow.V2.Context
{
ContextName = new ContextName(agent, model.sessionId, "my-input-context-data"),
LifespanCount = 3,
Parameters = new Struct
{
Fields = {
{ "Source", Value.ForString(model.Source) },
{ "UserId" , Value.ForString(model.UserId.ToString())},
{ "Name" , Value.ForString(model.FirstName)}
}
}
};
var request = new DetectIntentRequest
{
SessionAsSessionName = new SessionName(agent, model.sessionId),
QueryParams = new QueryParameters
{
GeoLocation = new LatLng {Latitude = model.Latitude, Longitude = model.Longitude},
TimeZone = model.TimeZone ?? "MST"
},
QueryInput = query
};
request.QueryParams.Contexts.Add(commonContext);
// ------------
var creds = GetGoogleCredentials("myCredentials.json");
var channel = new Grpc.Core.Channel(SessionsClient.DefaultEndpoint.Host, creds.ToChannelCredentials());
var client = SessionsClient.Create(channel);
var response = client.DetectIntent(request);
channel.ShutdownAsync();
return response;
Note: I included the explicit ShutDownAsync (it's not in an async call) because I was getting some file locking issues when attempting to re-deploy the WebAPI project (and only after having executed this code).
Thanks
Chris
Updated 4/25: The most basic way I use this is to integrate the user's name into intent responses:
It can also be read from within the webhook/inline fulfillment index.js:
const name = request.body.queryResult && request.body.queryResult.outputContexts && request.body.queryResult.outputContexts[0].parameters.Name

Categories