Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating an Incident Management Bot with MSAL, Graph API, and Python

0.00/5 (No votes)
3 Dec 2021 1  
In this article we will create an app that can create a new Teams Channel and invite stakeholders to provide updates to assist with incident management.
Here we will demonstrate how to create a Flask web app that creates an incident management channel in Teams, invites incident stakeholders to the channel, and publishes status updates to the channel to keep stakeholders informed.

This article will demonstrate how to create a Flask web app with the following functionality:

  • It creates an incident management channel in Teams.
  • It then invites incident stakeholders to the channel.
  • Publishes status updates to the channel to keep stakeholders informed.

Whether an enterprise offers customer-facing web apps or creates internal line-of-business apps, outages and other incidents are serious and stakeholders want to be kept up to date in real time. Many incident management tools offer integration with Teams, but they don’t always provide all the features enterprises need.

Teams has become the go-to communication software package for the workplace and provides an ideal platform to keep all involved parties informed throughout the lifecycle of the incident — from raising the alert to giving timely updates and estimates, it ensures that information is readily accessible to those who need it.

Our Flask app will be able to create a Teams channel and invite a list of stakeholders. The app will also offer a simple UI that an SRE or other incident commander can use to publish status updates to the channel on their behalf. With a single click, the incident commander can easily keep stakeholders updated without removing themselves from their workflow to type chat messages.

To participate, you will need:

  • The project prerequisites, listed in our first article
  • The code we have created so far in articles 1 and 2
  • A Microsoft Teams account

You can examine the complete code for this project on Github.

Adapt the Config

Our app will be accessing our Teams, so it needs permission to create channels, post to the channel and access user details. Update the permissions on Azure AD and edit our app_config.py changing the SCOPE configuration item as follows:

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

Create the Entry Point

Let’s update index.html in templates/ to create a button as an entry point to our Teams interface.

Add the following code:

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

When selected, this button will make a call to the teams_demo function, which we’ll add to app.py in a moment. This checks the user is logged in, and returns them to the login page if they are not.

It gets the Team_ID from the configuration file app_config.py, which the user needs to set up and update before running the app. It should match this:

Python
TEAM_ID = "Enter_the_Team_Id_Here"

Most companies have a dedicated in-house team for incident management support. Team IDs can be found by running the query https://graph.microsoft.com/v1.0/me/joinedTeams on Graph Explorer - Microsoft Graph.

Our teams_demo function takes this ID, retrieves the Team members and details, and gets them ready to render in HTML.

Add the following code to app.py:

Python
@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'))

We must also add our helper functions, which perform the calls to Microsoft Graph, to app.py:

Python
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()

Next, create the index.html page in templates/teams-demo/ to render everything:

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 %}

This HTML provides a form for our users to fill. It extends base.html, which we created in our first article. Users can add the channel name, which has been prefixed with IncidentChannel- to let users easily identify it.

Users can also add members to the channel. The list of members is already populated with the results of the Graph call from _get_team_members. Each is presented with the option to add a description.

Create the Channel

Once our HTML form has been submitted, we need to submit this user-configured channel to the Microsoft Graph API to create our Teams Channel. This requires another Graph call:

Python
@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

Once this Graph API post has been submitted, we need to create an interface for our user to post updates to this channel.

Let’s design our teams-demo/channel_mgt.html page in our templates directory:

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 %}

This creates a page that displays information about the incident channel, including:

  • Channel name
  • Channel description
  • Channel team members
  • Channel creation date and time

We’ve also created some selectable "status updates", with an optional text box for extra information.

Create the Status Update

To update the status, we need a new function in app.py. This function retrieves the status and any associated message from the form and post it to the channel, sending the cached token for verification — all if the user is logged in. Otherwise, the user will be redirected to the login page.

pyhton
@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()

When the status has been updated, our page is ready to send more messages to update our status again.

App in Action

To test our app, run:

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

Upon login, our menu looks like this:

To open a new Teams channel for our incident, we select the blue Teams Demo button and fill the form:

When we update the status and watch Teams, we see our updates coming through.

We can continue to keep all interested parties informed by posting to the newly created Teams channel, without interrupting workflow.

Next Steps

Our demo application is complete. There is plenty of scope for you to build your own extensions to these apps. You may wish to add, for example, a button to delete the channel, or experiment with the settings to adapt the Graph calls. You’ll likely find yourself beginning to feel like a product owner wanting more and more features.

Additionally, for brevity, we have not included much error handling. Calls should be put in try/except blocks for safety and a more informative user interface. You may wish to add these for your app.

Coding along with this series has demonstrated a few of the features accessible with Azure AD, MSAL, and Microsoft Graph API. There is still plenty more to explore!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here