65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 MSAL、Graph API 和 Python 创建事故管理机器人

starIconstarIconstarIconstarIconstarIcon

5.00/5 (2投票s)

2021 年 12 月 3 日

CPOL

5分钟阅读

viewsIcon

5937

在本文中,我们将创建一个应用程序,该应用程序可以创建新的 Teams 频道并邀请利益相关者提供更新,以协助进行事故管理。

本文将演示如何使用以下功能创建 Flask Web 应用程序

  • 它会在 Teams 中创建一个事故管理频道。
  • 然后,它会邀请事故相关方加入该频道。
  • 将状态更新发布到频道,以告知相关方。

无论企业是否提供面向客户的 Web 应用程序,还是创建内部业务线应用程序,停机和其他事故都是严重的,相关方希望实时了解情况。许多事故管理工具提供与 Teams 的集成,但它们并不总是提供企业所需的所有功能。

Teams 已成为工作场所的首选通信软件套件,为在事故的整个生命周期中让所有相关方了解情况提供了一个理想的平台——从发出警报到提供及时的更新和估计,它确保了需要信息的人能够轻松访问。

我们的 Flask 应用程序将能够创建一个 Teams 频道并邀请一个利益相关者列表。该应用程序还将提供一个简单的 UI,供 SRE 或其他事故指挥官代表他们将状态更新发布到频道。只需单击一下,事故指挥官就可以轻松地让相关方了解情况,而无需离开他们的工作流程来键入聊天消息。

要参与,您将需要

  • 我们第一篇文章中列出的项目先决条件
  • 我们在文章 1 和 2 中迄今为止创建的代码
  • Microsoft Teams 帐户

您可以在 Github 上查看此项目的 完整代码

调整配置

我们的应用程序将访问我们的 Teams,因此它需要创建频道、向频道发布消息和访问用户详细信息的权限。更新 Azure AD 上的权限,并编辑我们的 app_config.py 文件,将 SCOPE 配置项更改为如下所示

SCOPE = [
    "Channel.Create",
    "ChannelSettings.Read.All",
    "ChannelMember.ReadWrite.All",
    "ChannelMessage.Send",
    "Team.ReadBasic.All",
    "TeamMember.ReadWrite.All",
    "User.ReadBasic.All"
]

创建入口点

让我们更新 templates/ 目录下的 index.html 文件,创建一个按钮作为 Teams 界面的入口点。

添加以下代码

<a class="btn btn-primary btn-lg" href="/teams-demo" role="button">Teams Demo</a>

选择此按钮后,它将调用 teams_demo 函数,我们将在稍后将其添加到 app.py 中。此函数会检查用户是否已登录,如果未登录,则将他们重定向回登录页面。

它从 app_config.py 配置文件中获取 Team_ID,用户在运行应用程序之前需要设置并更新此文件。它应该与此匹配

TEAM_ID = "Enter_the_Team_Id_Here"

大多数公司都有专门的内部团队来支持事故管理。团队 ID 可以通过在Graph Explorer - Microsoft Graph上运行查询 https://graph.microsoft.com/v1.0/me/joinedTeams 来找到。

我们的 teams_demo 函数接受此 ID,检索团队成员和详细信息,并准备好在 HTML 中呈现。

将以下代码添加到 app.py

@app.route("/teams-demo")
def teams_demo():
    if not session.get("user"):
        return redirect(url_for("login"))
    team = _get_team(app_config.TEAM_ID)
    teamMembers = _get_team_members(app_config.TEAM_ID)
    return render_template('teams-demo/index.html',  team = team, teamMembers = teamMembers.get('value'))

我们还必须将执行 Microsoft Graph 调用的帮助函数添加到 app.py

def _get_team(id):
    token = get_token(app_config.SCOPE)        
    return requests.get(f"https://graph.microsoft.com/v1.0/teams/{id}",
        headers={'Authorization': 'Bearer ' + token['access_token']}).json()

def _get_team_members(teamId):
    token = get_token(app_config.SCOPE)        
    return requests.get(f"https://graph.microsoft.com/v1.0/teams/{teamId}/members",
    headers={'Authorization': 'Bearer ' + token['access_token']}).json()

接下来,在 templates/teams-demo/ 目录下创建 index.html 页面以呈现所有内容

{% extends "base.html" %}
{% block mainheader %}Teams Demo{% endblock %}
{% block content %}

<form action="/create-channel" method="POST">
  <div class="form-group row">
    <div class="col-5">
      <h2>Team: {{ team.displayName }}</h2>
    </div>
  </div>
  <div class="form-group row">
    <div class="col-5">
      <div class="input-group mb-2">
        <div class="input-group-prepend">
          <div class="input-group-text pr-1">IncidentChannel-</div>
        </div>
        <input type="text" class="form-control" name="channelName" placeholder="Channel name">
      </div>
    </div>
  </div>
  <div class="form-group row">
    <div class="col-5">
      <label for="members">Add members from team (Hold CTRL to select multiple)</label>
      <select multiple class="form-control" id="members" name="members">
        {% for member in teamMembers %}
        <option value="{{ member.userId }}">{{ member.displayName }}</option>
        {% endfor %}
      </select>
    </div>
  </div>
  <div class="form-group row">
    <div class="col-5">
      <div class="input-group mb-2">
        <input type="text" class="form-control" name="incidentDescription" placeholder="Short description of incident">
      </div>
    </div>
    <div class="col-2">
      <button type="submit" class="btn btn-primary btn-md mb-2">Create Channel</button>
    </div>
  </div>
</form>
{% endblock %}

此 HTML 提供了一个供用户填写的表单。它扩展了我们在第一篇文章中创建的 base.html。用户可以添加频道名称,该名称已预置 IncidentChannel- 前缀,以便用户轻松识别。

用户还可以添加成员到频道。成员列表已填充了 _get_team_members 的 Graph 调用结果。每个成员都可以选择添加描述。

创建频道

一旦我们的 HTML 表单提交,我们就需要将用户配置的频道提交给 Microsoft Graph API 来创建我们的 Teams 频道。这需要另一个 Graph 调用

@app.route("/create-channel", methods=["POST"])
def create_channel():
    if not session.get("user"):
        return redirect(url_for("login"))
    channelName = f"IncidentChannel-{request.form.get('channelName')}"
    incidentDescription = request.form.get('incidentDescription')
    members = request.form.getlist('members')
    teamId = app_config.TEAM_ID
    members_list = _build_members_list(members)
    token = get_token(app_config.SCOPE)
    channel = requests.post(f"https://graph.microsoft.com/v1.0/teams/{teamId}/channels", json={
        "displayName": channelName,
        "description": incidentDescription,
        "membershipType": "private",
            "members": members_list
            },
        headers={'Authorization': 'Bearer ' + token['access_token'], 'Content-type': 'application/json'}).json()
    channelMembers = _get_channel_members(teamId, channel.get('id'))
    return render_template('teams-demo/channel_mgt.html', channel = channel, channelMembers = channelMembers.get('value'))

 def _build_members_list(members):
    members_list = []
    for memberId in members:
        members_list.append(
                    {
                    "@odata.type":"#microsoft.graph.aadUserConversationMember",
                    "user@odata.bind":f"https://graph.microsoft.com/v1.0/users('{memberId}')", # add authenticated user
                    "roles":["owner"]
                    })
    return members_list

一旦提交了此 Graph API POST 请求,我们就需要为用户创建一个界面,以便他们可以将更新发布到此频道。

让我们在 templates 目录中设计我们的 teams-demo/channel_mgt.html 页面

{% extends "base.html" %}
{% block mainheader %}Teams Demo{% endblock %}
{% block content %}
<div class="container">
    <div class="row">
        <div class="col-sm-6">
            <div class="card">
                <div class="card-header">
                    Channel Details
                </div>
                <div class="card-body">
                    <p>Channel name: {{ channel.displayName }}</p>
                    <p>Channel desc: {{ channel.description }}</p>
                    <p>Created: {{ channel.createdDateTime }}</p>
                </div>
            </div>
        </div>
        <div class="col-sm-6">
            <div class="card">
                <div class="card-header">
                    Channel Members
                </div>
                <div class="card-body">
                    <ul>
                    {% for member in channelMembers %}
                    <li>{{ member.displayName }}</li>
                    {% endfor %}
                </ul>
                </div>
            </div>
        </div>
    </div>
    <div class="row mb-3"></div>
    <div class="row">
        <div class="col-sm-12">
        <div class="card">
            <div class="card-header">
                Issue a Status Update
            </div>
            <div class="card-body">
                <form action="/status-update" method="POST">
                    <input type="hidden" id="channelId" name="channelId" value="{{ channel.id }}">
                    <div class="form-group row">
                        <div class="col-auto">
                            <select class="custom-select" name="status" required>
                                <option value="Status Update - Issue being investigated">Status Update - Issue being investigated</option>
                                <option value="Status Update - Issue diagnosed">Status Update - Issue diagnosed</option>
                                <option value="Status Update - Issue resolved">Status Update - Issue resolved</option>
                            </select>
                        </div>
                    </div>
                    <div class="form-group row">
                        <div class="col-5">
                            <div class="input-group mb-2">
                                <input type="text" class="form-control" name="message"
                                    placeholder="Additional message">
                            </div>
                        </div>
                        <div class="col-2">
                            <button type="submit" class="btn btn-primary btn-md mb-2">Update Status</button>
                        </div>
                    </div>
            </div>
        </div>
        </div>
    </div>
</div>
{% endblock %}

这会创建一个页面,显示有关事故频道的信息,包括

  • 频道名称
  • 频道描述
  • 频道团队成员
  • 频道创建日期和时间

我们还创建了一些可选的“状态更新”,并附带一个可选的文本框用于额外信息。

创建状态更新

要更新状态,我们需要在 app.py 中添加一个新函数。此函数从表单中检索状态和任何相关消息,并将其发布到频道,发送缓存的令牌进行验证——所有这些的前提是用户已登录。否则,用户将被重定向到登录页面。

@app.route("/status-update", methods=["POST"])
def status_update():
    if not session.get("user"):
        return redirect(url_for("login"))
    statusUpdate = request.form.get('status')
    additionalMessage = request.form.get('message')
    channelId = request.form.get('channelId')
    token = get_token(app_config.SCOPE)
    requests.post(f"https://graph.microsoft.com/v1.0/teams/{app_config.TEAM_ID}/channels/{channelId}/messages", json={
        "body": {
        "content": f"{statusUpdate} - {additionalMessage}"
        }},
        headers={'Authorization': 'Bearer ' + token['access_token'], 'Content-type': 'application/json'}).json()
    channel = _get_channel(app_config.TEAM_ID, channelId)
    channelMembers = _get_channel_members(app_config.TEAM_ID, channelId)
    return render_template('teams-demo/channel_mgt.html', channel = channel, channelMembers = channelMembers.get('value'))


def _get_channel(teamId, channelId):
    token = get_token(app_config.SCOPE)        
    return requests.get(f"https://graph.microsoft.com/v1.0/teams/{teamId}/channels/{channelId}",
        headers={'Authorization': 'Bearer ' + token['access_token']}).json()

状态更新后,我们的页面已准备好发送更多消息以再次更新状态。

应用运行

要测试我们的应用程序,请运行

flask run --port=5000 --host=localhost

登录后,我们的菜单如下所示

要为我们的事故打开一个新的 Teams 频道,我们选择蓝色的 Teams Demo 按钮并填写表单

当我们更新状态并查看 Teams 时,我们会看到我们的更新正在进行。

我们可以继续通过发布到新创建的 Teams 频道来让所有相关方了解情况,而不会中断工作流程。

后续步骤

我们的演示应用程序已完成。您有很多机会可以构建自己的应用程序扩展。例如,您可能希望添加一个删除频道的按钮,或者尝试调整设置以适应 Graph 调用。您可能会发现自己开始像产品所有者一样想要越来越多的功能。

此外,为了简洁起见,我们没有包含太多错误处理。为了安全性和更具信息量的用户界面,应将调用放在 try/except 块中。您可能希望为您的应用程序添加这些。

跟随本系列进行编码展示了 Azure AD、MSAL 和 Microsoft Graph API 提供的一些功能。仍有许多内容有待探索!

© . All rights reserved.