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:
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:
<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:
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:
@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:
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:
{% 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:
@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}')",
"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:
{% 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.
@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!