November 27th, 2018

30DaysMSGraph – Day 27 – Use case: Create a Team

List of all posts in the #30DaysMSGraph series

-Today’s post written by Nick Kramer

In Day 26 we concluded calling Microsoft Graph using Flow.  Today, we’ll show how to create teams and automate team lifecycles.

Introduction to Microsoft Teams

Microsoft Teams allows people to work together and get projects done by collaborating through chat, calls, and online meetings. Each team has members and owners, channels that contain chat messages, files (stored in SharePoint by default), apps, and tabs. Some of the apps and tabs you might use in a team include OneNote notebooks, Planner plans, and SharePoint lists.

Teams and Groups

Every team is part of a Group. (But not every group has a team!) Some of the information you think of as part of the team is actually part of the Group object, such as the name of the team/group and its members and owners. When you create a new team, you typically create a new group as well. (But it’s also possible to add a team to an existing group)

When creating teams and groups, you need the Group.ReadWrite.All scope. Both user delegated permissions and application permissions are supported.

Clone the Sample Application

To follow along with the code please clone the sample application from the contoso-airlines-teams-sample repo and follow the instructions in the readme.md file to build the project. The interesting code is in GraphService.cs.

In the Contoso Airlines sample, we create new teams every day for each flight that Contoso Airlines flies — and then archive the team after the flight is over.

Creating the team

Before we create a team, we need the IDs of the team members. Given the user’s universal principle name (UPN), we can retrieve the user’s data, including their ID:

String userId = (await HttpGet<User>($"/users/{upn}")).Id;

When we add a user to a team, Graph needs to know the full URL to that user:

String fullUrl = $"https://graph.microsoft.com/v1.0/users/{userId}";

We repeat that for all the users in the flight crew:

List<string> ownerIds = await GetUserIds(ownerUpns);
List<string> memberIds = await GetUserIds(flight.crew);

Armed with that list of users, we can now create the group by providing the display name, mail nickname, description, visibility, owners, and members:

            Group group =
                (await HttpPost($"/groups",
                            new Group()
                            {
                                DisplayName = "Flight " + flight.number,
                                MailNickname = "flight" + GetTimestamp(),
                                Description = "Everything about flight " + flight.number,
                                Visibility = "Private",
                                Owners = ownerIds,
                                Members = memberIds,
                                GroupTypes = new string[] { "Unified" }, // same for all teams
                                MailEnabled = true,                      // same for all teams
                                SecurityEnabled = false,                 // same for all teams
                            }))
                .Deserialize<Group>();

This generates a JSON payload of the form:

{
    "displayName":"Flight 157",
    "mailNickname":"flight157",
    "description":"Everything about flight 157",
    "visibility":"Private",
    "groupTypes":["Unified"],
    "mailEnabled":true,
    "securityEnabled":false,
    "members@odata.bind":[
        "https://graph.microsoft.com/v1.0/users/bec05f3d-a818-4b58-8c2e-2b4e74b0246d",
        "https://graph.microsoft.com/v1.0/users/ae67a4f4-2308-4522-9021-9f402ff0fba8",
        "https://graph.microsoft.com/v1.0/users/eab978dd-35d0-4885-8c46-891b7d618783",
        "https://graph.microsoft.com/v1.0/users/6a1272b5-f6fc-45c4-95fe-fe7c5a676133"
    ],
    "owners@odata.bind":[
        "https://graph.microsoft.com/v1.0/users/6a1272b5-f6fc-45c4-95fe-fe7c5a676133",
        "https://graph.microsoft.com/v1.0/users/eab978dd-35d0-4885-8c46-891b7d618783"
    ]
}

(Note the members@odata.bind syntax to provide references to existing resources, in this case users)

The next step is to create a team within the group:

await HttpPut($"/groups/{group.Id}/team",
    new Team()
    {
        GuestSettings = new TeamGuestSettings()
        {
            AllowCreateUpdateChannels = false,
            AllowDeleteChannels = false
        }
    },
retries: 3, retryDelay: 10);

We could pass an empty body to PUT /groups/{group.Id}/team, but we prefer to change some of the default settings – we don’t want guests to be able to create, update, or delete channels. Note also that sometimes when the team is created immediately after the group, that request can fail with a 404 because the necessary data hasn’t replicated to all data centers, so we retry in that case after a 10 second delay.

Note that the team has the same ID as the group:

string teamId = group.Id; // always the same

Channels

Teams contain a list of channels, which contain the team’s chat messages. Channels also have a Files tab (typically linked to SharePoint), as well as any additional tabs you want to add to the team. Let’s create a channel for pilot talk:

Channel channel = (await HttpPost(
    $"/teams/{teamId}/channels",
    new Channel()
    {
        DisplayName = "Pilots",
        Description = "Discussion about flightpath, weather, etc."
    }
)).Deserialize<Channel>();

When you create a channel with POST /teams/{teamId}/channels, Graph returns the created channel so you know its ID.

Tabs

Let’s add a tab to channel that shows a map of the airport. Every tab has an associated app, for a map we can use the Website app. We know from documentation that the app ID for the Website app is “com.microsoft.teamspace.tab.web”. Graph will want that as a reference:

var appReference = $"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/{appid}";

Using that, we can create the tab:

await HttpPost($"/teams/{teamId}/channels/{channel.Id}/tabs",
new TeamsTab()
{
    DisplayName = "Map",
    TeamsApp = appReference, // It's serialized as "teamsApp@odata.bind" : appReference
    Configuration = new TeamsTabConfiguration()
    {
        EntityId = null,
        ContentUrl = "https://www.bing.com/maps/embed?h=800&w=800&cp=47.640016~-122.13088799999998&lvl=16&typ=s&sty=r&src=SHELL&FORM=MBEDV8",
        WebsiteUrl = "https://binged.it/2xjBS1R",
        RemoveUrl = null,
    }
});

In the configuration section of the tab, there’s four properties that every tab has – the EntityID, the ContentURL, the WebsiteUrl, and the RemoveURL. At its core, a tab is an HTML rendering surface – and ContentURL is the HTML the render. WebsiteUrl is the HTML the show if the user clicks the “Go to website” button. RemoveUrl is the HTML shown when the user clicks the Remove button. And EntityId is a string that means something only to the app that powers the tab (in this case, the website app leaves it null) – again, see the docs for your favorite app.

Create a SharePoint list

Next, let’s create a SharePoint list of the problem passengers on the flight, and pin it to a tab. We’ll need some additional permission scopes in order to use SharePoint: Sites.ReadWrite.All and Sites.Manage.All. With those, we can get to the SharePoint site using the group:

var teamSite = await HttpGet<Site>($"/groups/{groupId}/sites/root",
retries: 3, retryDelay:30);

(Again, there can be replication issues if you access the site to quickly after creating the group, so we put in some retries)

Once we have the site, we can create a new SharePoint List with 3 columns, Name, SeatNumber, and Notes:

var list = (await HttpPost($"/sites/{teamSite.Id}/lists",
new SharePointList
{
    DisplayName = "Challenging Passengers",
    Columns = new List<ColumnDefinition>()
    {
        new ColumnDefinition
        {
            Name = "Name",
            Text = new TextColumn()
        },
        new ColumnDefinition
        {
            Name = "SeatNumber",
            Text = new TextColumn()
        },
        new ColumnDefinition
        {
            Name = "Notes",
            Text = new TextColumn()
        }
    }
}))
.Deserialize<SharePointList>();

That creates an empty list with those columns. Next we add some data to it:

await HttpPost($"/sites/{teamSite.Id}/lists/{list.Id}/items",
    challengingPassenger
    );

 

Then we add a tab to the channel we created earlier:

await HttpPost($"/teams/{groupId}/channels/{channelId}/tabs",
    new TeamsTab
    {
        DisplayName = "Challenging Passengers",
        TeamsApp = appReference, // It's serialized as "teamsApp@odata.bind" : appReference
        Configuration = new TeamsTabConfiguration
        {
            ContentUrl = list.WebUrl,
            WebsiteUrl = list.WebUrl
        }
    });

And now we have a fully functioning team, with members, channels, and tabs.

Apps

With user delegated permissions, you can also add an app to the team. (We are working on application permissions) Let’s add the SurveyMonkey bot to the team. We need to know the app ID of the app we want, we can figure that out by running this in Graph Explorer:

GET https://graph.microsoft.com/beta/teams/{sampleTeam.id}/installedApps?$expand=teamsAppDefinition&filter=teamsAppDefinition/displayName eq 'SurveyMonkey'

where {sampleTeam.id} is a test team that you added SurveyMonkey to using Microsoft Teams. Look for the teamsAppId in the response.

Now, using that value, we install survey monkey to the new team:

await HttpPost($"/teams/{team.Id}/installedApps",
    "{ \"teamsApp@odata.bind\" : \"" + graphV1Endpoint + "/appCatalogs/teamsApps/" + teamsAppId + "\" }");

Clone a team

Alternately, instead of creating a team from scratch, we can make a copy of an existing team. This is a good way to allow admins and end users to change the structure of the team you’re creating without having to update code. Clone Team also supports application permissions, so it’s a great way to get new teams with apps preinstalled.

When you clone a team, you get to pick which parts you want to clone. Your choices are apps, settings, channels, tabs, and members. We’ll choose “apps,settings,channels”:

var response = await HttpPostWithHeaders($"/teams/{flight.prototypeTeamId}/clone",
    new Clone()
    {
        displayName = "Flight 4" + flight.number,
        mailNickName = "flight" + GetTimestamp(),
        description = "Everything about flight " + flight.number,
        teamVisibilityType = "Private",
        partsToClone = "apps,settings,channels"
    });

Clone is a long-running operation, so we are actually creating a clone request and getting back a handle to check on its status. The critical part of the response is the Location header:

string operationUrl = response.Headers.Location.ToString();

We query that operationUrl to see if the cloning is done. If it’s not, we wait 10 seconds and repeat.

for (; ; )
{
    TeamsAsyncOperation operation = await HttpGet<TeamsAsyncOperation>(operationUrl);
    if (operation.Status == AsyncOperationStatus.Failed)
        throw new Exception();

    if (operation.Status == AsyncOperationStatus.Succeeded)
    {
        teamId = operation.targetResourceId;
        break;
    }

    Thread.Sleep(10000); // wait 10 seconds between polls
}

Then, because we chose not to clone the membership, we need to add some people to the team:

// Add the crew to the team
foreach (string id in flight.crew)
{
    string payload = $"{{ '@odata.id': '{graphV1Endpoint}/users/{id}' }}";
    await HttpPost($"/groups/{teamId}/members/$ref", payload);
    if (upn == flight.captain)
        await HttpPost($"/groups/{teamId}/owners/$ref", payload);
}

And that gives us a fully functioning team, with members and apps.

Archive

Finally, after the flight is over and we are done with the team, we can archive it. This will put the team into a read-only state and hide it from our list of teams so our UI doesn’t get cluttered, but we can still read the contents of the team via the gear icon at the bottom of the screen:

Like clone, archiving is a long-running operation, so we will create an archiving request and monitor its status:

HttpResponse response = await HttpPostWithHeaders($"/teams/{teamId}/archive", "{}");
string operationUrl = response.Headers.Location.ToString();

And like clone, we query that operationUrl to see if the archiving is done:

for (; ; )
{
    var operation = await HttpGet<TeamsAsyncOperation>(operationUrl);

    if (operation.Status == AsyncOperationStatus.Failed)
        throw new Exception();

    if (operation.Status == AsyncOperationStatus.Succeeded)
        break;

    Thread.Sleep(10000); // wait 10 seconds between polls
}

And there we go – we’ve shown how to create a new team, set it up with channels, tabs, and apps, and when the team has outlived its usefulness, how to archive it to make room for the next team.

Try it Out

Day 27 repo link

 

Join us tomorrow as we leverage webhooks for Microsoft Graph requests in Day 28.