本篇我们介绍Microsoft Teams相关的内容。
Microsoft Teams介绍
Microsoft Teams是用于企业沟通协作的软件,可以即时消息,语音通话,在线会议等。它以团队为基础单位,每个团队都有一些团队成员和团队所有者,不同的频道,频道中会包含聊天消息、文件(默认存储在SharePoint)、应用和标签。应用和标签可以是OneNote的笔记本、Planner的计划和SharePoint的列表。
团队和(Office 365)组
每个团队都是组的一部分,但并不是每个组都有团队。事实上有些我们以为是团队的信息其实是组对象的一部分,如团队的名字,成员和所有者。当一个新的团队被创建时,其实也创建了一个组(当然,将团队添加到现有的组也是可以的)。
创建组需要Group.ReadWrite.All权限,应用程序权限或用户托管权限都行。
克隆示例代码
示例代码的地址为
https://github.com/microsoftgraph/contoso-airlines-teams-sample
读者可以参照上面readme.md文件的介绍去注册应用程序和编译项目,主要的代码在GraphService.cs这个文件中。个人觉得这个示例代码的场景很不错,因此直接用它做介绍。另外需要说明的是,请重新在应用程序注册那里创建一个应用程序,因为之前创建的时候我们选择的帐户类型是组织内的,后期无法修改为允许个人微软帐户的设置。
在示例中,会每天为这个虚拟的Contoso航空的每个航班创建一个团队,并在航班结束后对团队进行归档。
创建团队
在创建团队之前,我们需要团队成员的ID。通过用户的UPN名称可以获取用户的数据,包括ID:
String userId = (await HttpGet<User>($"/users/{upn}")).Id;
当我们向团队添加用户时,Graph需要知道指向该用户的完整URL:
String fullUrl = $"https://graph.microsoft.com/v1.0/users/{userId}";
向所有要添加的用户应用一下步骤。
List<string> ownerIds = await GetUserIds(ownerUpns);
List<string> memberIds = await GetUserIds(flight.crew);
有了这些用户列表,,现在我们可以根据输入参数 (如显示名、邮箱昵称、描述、可见性、所有者和成员等)创建一个组。
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>();
创建成功会返回JSON格式的结果。
{
"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"
]
}
注意结果中members@data.bind部分的语法,它提供了对现有资源的引用,在本例中的话就是用户。
下一步就是基于这个组创建团队了。
await HttpPut($"/groups/{group.Id}/team",
new Team()
{
GuestSettings = new TeamGuestSettings()
{
AllowCreateUpdateChannels = false,
AllowDeleteChannels = false
}
},
retries: 3, retryDelay: 10);
如果以最简单的方式,本来是可以直接用PUT方法发送空的BODY到/groups/{group.Id}/team的,但我们想要更改一些默认的设置,比如我们不希望访客具有创建、更新和删除频道的权限,因此使用上面的代码,对属性进行了设置。
另外为什么要使用重试逻辑并延迟10秒呢?这是因为如果在组创建之后立即创建团队的话,由于一些必要的数据可能没有复制到所有的数据中心,我们会得到404的错误。
还有就是团队跟组的ID是一样的。
string teamId = group.Id; // always the same
频道
团队包含多个频道,团队的聊天消息就存在于这些频道中。频道中还有文件选项卡 (通常是链接到SharePoint) 和其他我们自行添加的选项卡。下面的代码示例我们创建一个用于飞行员沟通的频道。
Channel channel = (await HttpPost(
$"/teams/{teamId}/channels",
new Channel()
{
DisplayName = "Pilots",
Description = "Discussion about flightpath, weather, etc."
}
)).Deserialize<Channel>();
当我们发送POST请求到/teams/{teamId}/channels去创建频道时,Graph会返回被创建的频道,因此我们可以得到频道的ID。
选项卡
接下来我们向频道添加一个选项卡,该选项卡用于展示机场的地图。每个选项卡都有一个与之关联的应用,对于地图我们可以使用网站类型应用,即
“com.microsoft.teamspace.tab.web”
var appReference = $"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/{appid}";
创建选项卡我们可以使用如下的代码。
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,
}
});
选项卡实际上是HTML渲染出来的。在选项卡的配置部分,有四个选项卡的通用属性:
- ContentURL - 要渲染的内容的URL
- WebsiteUrl - 在用户点击访问网站按钮时要打开链接的URL
- RemoveUrl - 在用户点击删除按钮时要打开链接的URL
- EntityId - 字符串,表示应用的标识
更多关于选项卡的类型信息,访问下面的链接查看。
https://docs.microsoft.com/en-us/graph/teams-configuring-builtin-tabs
创建SharePoint列表
下面我们创建一个SharePoint列表用于记录航行过程中出现问题的乘客,并将它贴到一个选项卡上。
要使用SharePoint我们需要额外的权限:
- Sites.ReadWrite.All
- Sites.Manage.All
我们可以通过组获取到SharePoint网站。
var teamSite = await HttpGet<Site>($"/groups/{groupId}/sites/root",
retries: 3, retryDelay:30);
这里用重试逻辑和延迟的原因跟上面一样。
得到网站对象之后,我们可以创建一个新的SharePoint列表,这个列表有三个字段:
Name、SeatNumber和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>();
列表创建好之后,我们添加一些示例数据。
await HttpPost($"/sites/{teamSite.Id}/lists/{list.Id}/items",
challengingPassenger
);
最后我们添加一个选项卡到之前创建的频道上。
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
}
});
到现在,我们就有了一个完整功能的团队了,有成员、有频道、还有选项卡。
应用
使用用户托管权限,我们也可以向团队添加应用。比如我们将SurveyMonkey这个应用加到团队。我们需要知道应用的ID,在Graph Explorer中使用类似下面的链接可以获取到。
<strong>GET</strong> https://graph.microsoft.com/beta/teams/{sampleTeam.id}/installedApps?$expand=teamsAppDefinition&filter=teamsAppDefinition/displayName eq 'SurveyMonkey'
记得将团队的id换成我们自己的。
接下来用这个id在我们的新团队中安装SurveyMonkey应用。
await HttpPost($"/teams/{team.Id}/installedApps",
"{ \"teamsApp@odata.bind\" : \"" + graphV1Endpoint + "/appCatalogs/teamsApps/" + teamsAppId + "\" }");
克隆团队
比起从无到有创建一个团队,我们还可以克隆现有的团队。对于管理员和终端用户来说,这可以改变我们创建的团队的体系结构而不需要更新代码,是个很好的方式。克隆团队也支持应用程序权限,试想我们可以直接做出一个安装好应用的团队是多么棒的体验。
在本文的示例中,对于每个航班都是结构相同的团队,因此我们理所当然地采用克隆操作。克隆的强大之处还在于我们可以选择想要克隆的部分,包括应用、设置、频道、选项卡和成员。这里我们选择应用、设置和频道。
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"
});
克隆需要花费一些时间,因此我们这里创建一个异步的克隆请求,在回调中检查它的状态。响应的关键部分是Location,我们就是通过它来查询克隆是不是完成了,如果没完成则等待10秒然后再查询。
string operationUrl = response.Headers.Location.ToString();
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
}
由于我们在克隆时没有选择克隆成员关系,因此这里还需要添加一些成员。
// 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);
}
归档
正如开篇说的,航班结束之后要对团队进行归档。归档操作会使团队变为只读状态并在团队列表中被隐藏。我们可以通过右下角的齿轮图标读取归档团队的内容。
跟克隆类似,归档也需要一些时间。
HttpResponse response = await HttpPostWithHeaders($"/teams/{teamId}/archive", "{}");
string operationUrl = response.Headers.Location.ToString();
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
}
更多关于Graph中Teams API的内容,访问下面的链接。
https://aka.ms/teamsgraph/v1
这就是今天要介绍的全部内容了,enjoy!
备注 Demo中的提示
#1 Could not find a part of the path ‘…contoso-airlines-teams\project\bin\roslyn\csc.exe’
[Solution]
https://stackoverflow.com/questions/32780315/could-not-find-a-part-of-the-path-bin-roslyn-csc-exe
#2 Code中有写死的Tenant,需要更改为自己的,格式为yourdomain.onmicrosoft.com
#3 Code中有写死的用户名,用于指定机长和工作人员,记得在Flight构造函数处修改为自己租户中存在的用户。
captain = $"aaa@{tenantName}";
crew = new string[] {
$"bbb@{tenantName}",
$"ccc@{tenantName}",
};
this.admin = admin;
#4 如果要使用克隆功能的话,记得将代码中hard code的prototypeTeamId改为自己创建出来的team的。可以直接通过Graph浏览器获取。