Monday, 19 December 2022

Building a Microsoft Teams app: Posting a message to Teams on behalf of the current user

If you are building a Teams app and want to integrate with Teams channel messages, this post will be helpful to you. Specifically, we will be looking at posting a Teams channel message, from the app, on behalf of the currently logged in user.  For example, there could be a scenario where an event occurs within the Teams app and as a result a message needs to be posted to a Teams channel but instead of it coming from a bot, it needs to be posted by the logged in user's account.

Let's see how this can be done. 

1. Azure AD app and Permissions

First thing we need is an Azure AD app setup with the Microsoft Graph ChannelMessage.Send delegated permission. This permission is needed for posting messages to Teams using the current user credentials.

I should mention setting up the right permissions is part of a larger configuration in the Azure AD app needed for Single Sign On (SSO) setup in Teams app. You can see the full configuration here: Register your tab app with Azure AD - Teams | Microsoft Learn 


2. Current user's id token from Teams JS SDK v2 

Once the permissions are setup, we need to setup our frontend so that it can grab the current user's id token from Microsoft Teams. More info on Azure AD id tokens here: Microsoft identity platform ID tokens - Microsoft Entra | Microsoft Learn 

Although this token is available from Teams JS SDK v2, it cannot itself be used to make graph calls. We need to exchange it for a Microsoft Graph access token. For this we will send the id token to our backend:

const _postToTeams = async () => {
const idToken = await microsoftTeams.authentication.getAuthToken();
const teamsContext = await microsoftTeams.app.getContext();
const teamId = teamsContext.team?.groupId;
const channelId = teamsContext.channel?.id;
const message = "This message will be posted to Teams using Microsoft Graph";
await fetch(`/PostToTeams?teamId=${teamId}&channelId=${channelId}&message=${message}`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${idToken}`
}
});
}
view raw posttoteams.ts hosted with ❤ by GitHub

3. Getting Microsoft Graph delegated access token and posting message to Teams

It is recommended to do the token exchange as well as any further Graph calls from the backend of your app instead passing the Graph access token back to the frontend and making the calls from there:

public async Task<ActionResult> PostToTeams(string teamId, string channelId, string message)
{
var httpContext = _httpContextAccessor.HttpContext;
httpContext.Request.Headers.TryGetValue("Authorization", out StringValues assertion);
var idToken = assertion.ToString().Split(" ")[1];
var handler = new JwtSecurityTokenHandler();
var jwtSecurityToken = handler.ReadJwtToken(idToken);
string tenantId = jwtSecurityToken.Claims.First(claim => claim.Type == "tid").Value;
GraphServiceClient graphServiceClient = await GetDelegatedGraphServiceClient(idToken, tenantId, new string[] {
"https://graph.microsoft.com/ChannelMessage.Send"
});
var chatMessage = new ChatMessage
{
Body = new ItemBody
{
Content = message
}
};
await graphServiceClient.Teams[teamId].Channels[channelId].Messages.Request().AddAsync(chatMessage);
return Ok();
}
private async Task<GraphServiceClient> GetDelegatedGraphServiceClient(string idToken, string tenantId, string[] scopes)
{
string clientId = _configuration.GetSection("AzureAd:ClientId")?.Value;
string clientSecret = _configuration.GetSection("AzureAd:AppSecret")?.Value;
IConfidentialClientApplication app = ConfidentialClientApplicationBuilder.Create(clientId)
.WithClientSecret(clientSecret)
.WithAuthority($"https://login.microsoftonline.com/{tenantId}")
.Build();
UserAssertion assert = new UserAssertion(idToken);
var responseToken = await app.AcquireTokenOnBehalfOf(scopes, assert).ExecuteAsync();
string accessToken = responseToken.AccessToken.ToString();
var graphServiceClient = new GraphServiceClient(new DelegateAuthenticationProvider((requestMessage) =>
{
requestMessage
.Headers
.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
return Task.CompletedTask;
}));
return graphServiceClient;
}
view raw posttoteams.cs hosted with ❤ by GitHub

In the code we first graph the id token from the Authorization header, then exchange the id token for a Microsoft Graph access token, then finally we are able to make a Graph call to post a message to Teams as the current user.


Hope this helps!

Wednesday, 23 November 2022

Building a custom Microsoft Teams tab: Show a native loading indicator

When building a custom Microsoft Teams tab with Teams manifest v1.7+, there is an option to natively show a loading indicator in Teams before our app loads. This can be helpful if your app needs to fetch any data before the first load.

There are two parts to showing the loading indicator: 

1. The showLoadingIndicator property in the Teams manifest needs to be set to true.
2. When the tab loads, the app.initialize() and app.notifySuccess() methods of the Teams JS SDK v2 should be called to indicate to Teams that the app is ready to load and to hide the loading indicator.

Now there are some "interesting" things to know about this. If you are using the Teams Developer Portal to configure your Teams app, then as of now the showLoadingIndicator property is set to true by default and there is no way to change this in the portal. You will have to download the app manifest and make the necessary changes in the json manually.

If you are just starting with Microsoft Teams development and are unaware of the loading indicator option, the error message shown on the tab is not much help in figuring out the issue. It simply says:

"There was a problem reaching the app"


This is because the showLoadingIndicator property has been set to true by the Teams Developer portal and we don't yet have our tab calling the app.initialize() and app.notifySuccess() methods of the SDK. So, Teams thinks that our app has failed to load and shows the error message.

There are two possible solutions to fixing this problem:

1. Set the showLoadingIndicator property to false manually in the manifest json. But the drawback to this approach then, is that no native loading indicator would be shown, and the users might have to stare at a blank screen until the content loads.

2. Let the showLoadingIndicator property be set to true and then make sure we call the app.initialize() and app.notifySuccess() methods of the SDK.

Manifest.json:



Tab react code:


There are further options possible with the loading indicator like hiding the loading indicator even if the app continues to load in the background and explicitly specifying loading failure. To see more details about those, the Microsoft docs are a good place to start: Create a content page - Teams | Microsoft Learn

Hope this helps!

Wednesday, 14 September 2022

Partially update documents in Azure Cosmos DB

I have been working with Cosmos DB for a while now and until recently, there was one thing which always annoyed me: When updating a JSON document stored in a container, there was no way to only modify a few selected properties of the document. 

The entire JSON document had to be fetched by the client first, then locally replace the properties to be updated, and then send the entire document back to Cosmos DB and replace the previous version of the document. 

This was always a challenge because first, it added more work for developers and second, there could be concurrency issues if multiple clients could be downloading multiple copies of the document and updating the data and sending back their copy.

But fortunately, now it's possible to partially update a document in Cosmos DB and basically do a PATCH operation while only sending the properties to be changed over the wire. 



So in this post, let's have a look at the different ways in which we can partially update documents in Cosmos DB:

Setting up by creating a Cosmos DB document

First, we will create our sample document using code. I should mention I am using v3.30.1 of Azure Cosmos DB .NET core package from nuget: Microsoft.Azure.Cosmos

var endpointUri = "<cosmosdb-endpoint-uri>";
var primaryKey = "<cosmosdb-primary-key>";
var databaseId = "UsersDB";
var containerId = "Users";
var partitionKey = "allusers";
var _cosmosClient = new CosmosClient(endpointUri, primaryKey);
var _databaseResponse = await _cosmosClient.CreateDatabaseIfNotExistsAsync(databaseId);
var _containerResponse = await _databaseResponse.Database.CreateContainerIfNotExistsAsync(containerId, $"/partitionKey");
var user = new User()
{
Id = userIdForDemo,
Name = "Vardhaman",
Department = "Product",
Projects = new Project[] { new Project { Name = "Halo" } },
Skills = new string[] { "M365", "Azure" },
PartitionKey = "allusers"
};
await _containerResponse.Container.CreateItemAsync<User>(user, new PartitionKey(partitionKey));

As you can see this is a simple document representing a User object with a few attached properties:


Now in order to only change a few properties in the object, we need to use the Partial document update feature in Azure Cosmos DB 

Update document properties

Now lets have a look at how we can modify and add properties to the User object:

//Add new property
ItemResponse<User> response = await _containerResponse.Container.PatchItemAsync<User>(
id: userIdForDemo,
partitionKey: new PartitionKey(partitionKey),
patchOperations: new[] {
PatchOperation.Add($"/location", "London")
});
//Update existing property
ItemResponse<User> response = await _containerResponse.Container.PatchItemAsync<User>(
id: userIdForDemo,
partitionKey: new PartitionKey(partitionKey),
patchOperations: new[] {
PatchOperation.Add($"/name", "Vardhaman Deshpande")
});
In the code, we are updating an existing property of the document as well as adding a new property. Also, we are only sending the properties to be modified over the wire. 


We can also send both properties in a single operation by adding the operations to the patchOperations array.

Update elements in array


Just like document properties, we can also update single elements in an array in the document. Elements can be updated, added, added at a specific index and also removed from an array:

//Add new array item at the start
ItemResponse<User> response = await _containerResponse.Container.PatchItemAsync<User>(
id: userIdForDemo,
partitionKey: new PartitionKey(partitionKey),
patchOperations: new[] {
PatchOperation.Add($"/skills/0", "Architecture")
});
//Add new array item at the specified index
ItemResponse<User> response = await _containerResponse.Container.PatchItemAsync<User>(
id: userIdForDemo,
partitionKey: new PartitionKey(partitionKey),
patchOperations: new[] {
PatchOperation.Add($"/skills/2", "Integration")
});
//Append new array item at the end of the array
ItemResponse<User> response = await _containerResponse.Container.PatchItemAsync<User>(
id: userIdForDemo,
partitionKey: new PartitionKey(partitionKey),
patchOperations: new[] {
PatchOperation.Add($"/skills/-", "B2B SaaS")
});
//Update array element at specified index using Set operation
ItemResponse<User> response = await _containerResponse.Container.PatchItemAsync<User>(
id: userIdForDemo,
partitionKey: new PartitionKey(partitionKey),
patchOperations: new[] {
PatchOperation.Set($"/skills/1", "Microsoft 365")
});

Update objects and their properties


The properties of a JSON document can themselves be objects as well. The Azure Cosmos DB patch operations can also be used to update properties of specific objects as well as modifying the objects themselves.

//Update object property from item
ItemResponse<User> response = await _containerResponse.Container.PatchItemAsync<User>(
id: userIdForDemo,
partitionKey: new PartitionKey(partitionKey),
patchOperations: new[] {
PatchOperation.Add($"/projects/0/name", "Halo Infinite")
});
//Add new item to object array
ItemResponse<User> response = await _containerResponse.Container.PatchItemAsync<User>(
id: userIdForDemo,
partitionKey: new PartitionKey(partitionKey),
patchOperations: new[] {
PatchOperation.Add($"/projects/-", new Project(){ Name="Need for Speed" } )
});
//Remove item at specified index from object array
ItemResponse<User> response = await _containerResponse.Container.PatchItemAsync<User>(
id: userIdForDemo,
partitionKey: new PartitionKey(partitionKey),
patchOperations: new[] {
PatchOperation.Remove($"/projects/0" )
});

Hope this helps! For more details, do have a look at the Microsoft docs for Azure Cosmos DB partial updates: https://docs.microsoft.com/en-us/azure/cosmos-db/partial-document-update

Monday, 7 February 2022

Working with the Microsoft Graph communications API in a Microsoft Teams meeting app

Following up on my previous post about Microsoft Teams meeting apps, let's now have a look at how we can invoke the Microsoft Graph in our meeting app. 

Specifically, we will be using the Microsoft Graph communications API to get all meeting participants.

When I was first looking at this scenario, I thought it would be quite straightforward as this seems to be quite a common use case for meeting apps. However, I was in for a bit of a surprise as that did not turn out to be the case. 

My thinking was that I would get a meetingId as part of the Teams JS SDK context object and I would use this meetingId to get details from the Graph. But it turns out that the meetingId which the Teams JS SDK provides is completely different than the meetingId recognised by that Microsoft Graph

To fetch the meetingId which the Graph recognises, we have to introduce the Bot Framework SDK to the mix and exchange the Teams JS SDK meetingId for the Graph meeting Id. 

So let's see how to achieve this using code:

1) Using Teams JS SDK, get user id, meeting id, tenant id

This bit is straightforward, in your Teams tab you can get the meeting context using the Teams JS SDK:

import * as microsoftTeams from "@microsoft/teams-js";
useEffect(() => {
microsoftTeams.initialize();
microsoftTeams.getContext(async function (context: microsoftTeams.Context) {
const rawResponse = await fetch(`/Meeting?userId=${context.userObjectId}&meetingId=${context.meetingId}&tenantId=${context.tid}`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
}
});
});
}, []);
view raw meetingapp.tsx hosted with ❤ by GitHub

2) Use Bot SDK to get Graph meeting ID from the Teams meeting id

Next, from your Teams Bot, call the /v1/meetings/{meeting-id} API. Couple of things you should know beforehand: 

This API is currently in developer preview. This means that it will only work for when the Microsoft Teams Clients have been switched to preview mode.

Secondly, we have to turn on Resource Specific Consent (RSC) for your app and request the required scopes. More details on both points here: https://docs.microsoft.com/en-us/microsoftteams/platform/apps-in-teams-meetings/api-references?tabs=dotnet#get-meeting-details-api

Once you have everything setup, you can call the API to get the meeting details:

public async Task<OnlineMeetingServiceResponse> GetMeetingDetails(string serviceUrl, string meetingId)
{
using var getRoleRequest = new HttpRequestMessage(HttpMethod.Get, new Uri(new Uri(serviceUrl), string.Format("v1/meetings/{0}", meetingId)));
getRoleRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await this.botCredentials.GetTokenAsync());
using var getRoleResponse = await this.httpClient.SendAsync(getRoleRequest);
getRoleResponse.EnsureSuccessStatusCode();
var meetingresponse = JsonConvert.DeserializeObject<OnlineMeetingServiceResponse>(await getRoleResponse.Content.ReadAsStringAsync());
return meetingresponse;
}

The response would contain a details object with an msGraphResourceId property. This will be the meeting id which would work with the Graph API.

{
"details": {
"id": "meeting ID",
"msGraphResourceId": "",
"scheduledStartTime": "2020-08-21T02:30:00+00:00",
"scheduledEndTime": "2020-08-21T03:00:00+00:00",
"joinUrl": "https://teams.microsoft.com/l/xx",
"title": "All Hands",
"type": "Scheduled"
},
//..//
}

3) Use Graph API to get meeting participants from Graph meeting id

Finally, from the msGraphResourceId, we can make a regular call to the Graph to get the meeting details. Specifically, we will call the /onlineMeetings/{meetingId} endpoint: https://docs.microsoft.com/en-us/graph/api/onlinemeeting-get?view=graph-rest-1.0&tabs=http

In this case, we will get the meeting participants:

GraphServiceClient graphClient = await GetGraphClient();
OnlineMeeting onlineMeeting = await graphClient.Users[userId].OnlineMeetings[meetingresponse.details.msGraphResourceId].Request().GetAsync();
MeetingParticipants participants = onlineMeeting.Participants;

Make sure you have the correct Delegated or Application permissions granted to the Azure AD app registration you are using for authenticating to the Graph.

This is not an ideal situation as you might not have a requirement for a Bot in your Teams app and might be building a tab for example. In this case, Yannick Reekmans has written a post for you here: https://blog.yannickreekmans.be/get-full-meeting-details-in-a-teams-meetings-app-without-using-bot-sdk/  

That's it for now. Hope you find this helpful when exploring meeting apps right now.

Thursday, 13 January 2022

Working with Apps for Microsoft Teams meetings

Microsoft Teams meetings extensions or "Apps for Teams meetings" are the newest entry in the Microsoft Teams extensibility story. They can be used to build custom experiences right into the meeting experience. Meeting participants are able to interact with the custom experiences either before, during or after the meeting. To know more about apps for Teams meetings, a great place to start is the Microsoft docs: https://docs.microsoft.com/en-us/microsoftteams/platform/apps-in-teams-meetings/teams-apps-in-meetings

Pre-meeting and Post-meeting experiences

The pre and post meeting experiences are not too different than what we are used to when building Tabs for Teams. The basic structure remains the same with the only different thing being that the Teams SDK provides the meeting specific APIs when invoked from a meeting app. These APIs can be used to get meeting details like participants and meeting context in our app. More information on the APIs here: https://docs.microsoft.com/en-us/microsoftteams/platform/apps-in-teams-meetings/api-references?tabs=dotnet


In-meeting experiences

When it comes to the In-meeting experiences, there are two main areas: The side panel and the in meeting dialog box (also known as content bubble). The side panel is used to show custom experiences while the meeting is in progress


And the in-meeting dialog box (or content bubble) is used to show content, prompt or collect feedback from the users during the meeting:


I should mention here that the In-meeting experiences only work in the Teams desktop and mobile clients as of now. They don't work in the Teams web browser interface at the time of this writing. To me this is the biggest challenge for using them in production.

Now that's enough introduction of the concepts, let's take a look at how to actually build these experiences and what are the moving pieces when building them. The Microsoft docs do provide some great step by step tutorials for each of the use cases. 

In this post we will look at the In-meeting dialog box. We will take a look at the Microsoft's code sample and walk through it. You can find the code sample here: https://github.com/OfficeDev/Microsoft-Teams-Samples/tree/main/samples/meetings-content-bubble/csharp

1) Configure an Azure Bot and enable Teams Channel

Create a bot in Azure and configure the endpoint which should receive the Teams events:


Next, add the Teams channel so that the bot is able to talk to Microsoft Teams:



2) Update the Teams manifest

For the In-meeting dialog box, we don't have to make any special changes in the manifest. We just need to make sure that since the dialog box will be shown via the bot, our Teams app manifest should have the bot configured as part of it. 

"bots": [
{
"botId": "aefd72ea-956f-4673-a9af-e9a39212b03e",
"scopes": [
"personal",
"team",
"groupchat"
],
"supportsFiles": false,
"isNotificationOnly": false
}
],
"validDomains": [
"meetingapps.ngrok.io"
]


3) Create an Azure AD app registration for the app


When we created the Azure Bot, a new Azure AD app registration was created behind the scenes as well. Grab the client id and client secret from this app as we would need it later to add to our bot config



4) Start the ngrok tunnel and update the code sample:

To debug locally, you will need to setup an ngrok tunnel to your local machine. More details here: https://ngrok.com/


And in the code sample, update the bot client id and client secret along with the ngrok tunnel url:



5) In meeting dialog code:

The way to bring up an in-meeting dialog is to use the regular Bot Framework turnContext.SendActivityAsync(activity) code but with updated Teams channel data:

Attachment adaptiveCardAttachment = GetAdaptiveCardAttachment("QuestionTemplate.json", agendaItem);
var activity = MessageFactory.Attachment(adaptiveCardAttachment);
activity.ChannelData = new TeamsChannelData
{
Notification = new NotificationInfo()
{
AlertInMeeting = true,
ExternalResourceUrl = $"https://teams.microsoft.com/l/bubble/{ _config["MicrosoftAppId"] }?url=" +
HttpUtility.UrlEncode($"{_config["BaseUrl"]}/ContentBubble?topic={agendaItem.Topic}") +
$"&height=270&width=250&title=ContentBubble&completionBotId={_config["MicrosoftAppId"]}"
}
};
await turnContext.SendActivityAsync(activity);
view raw botfxcode.cs hosted with ❤ by GitHub

You will notice that in the In-meeting dialog url, there is a reference to {_config["BaseUrl"]}/ContentBubble 

This means that the contents of the in-meeting dialog have to be hosted in our app. This is good news as that means we have complete control over what is displayed in the dialog. In this code sample, the contents are hosted in an MVC view:

@page
@model Content_Bubble_Bot.Pages.ContentBubbleModel
@{
ViewData["Title"] = "Layout";
Layout = "~/Pages/Shared/_Layout.cshtml";
}
<script>
microsoftTeams.initialize();
function validateForm() {
var feedbackInfo = {
Topic: '@Model.Topic',
Feedback: document.querySelector('input[name="inputVal"]:checked').value
}
microsoftTeams.tasks.submitTask(feedbackInfo);
return true;
}
</script>
<div class="login" style="color:aliceblue;font-family:'Segoe UI';text-align:left">
<form id="userLog" onSubmit="return validateForm()">
<h3> Provide your Feedback!</h3>
<b> <label id="reviewTitle">@Model.Topic</label></b><br /><br />
<input type="radio" id="yes" name="inputVal" value="Yes">
<label for="yes">Yes</label><br />
<input type="radio" id="no" name="inputVal" value="No">
<label for="no">No</label><br />
<br />
<input style="border-radius: 12px;display: inline-block;width:74px;height:29px;font-size:medium;text-align:center;align-items:center"
type="submit" value="Submit">
</form>
</div>

And once everything fits together, we can see the sample code running to show an In-meeting dialog launched in the context of a meeting:


Hope this helps, and thanks for reading!