Category: Office 365

Microsoft Teams: Creating A Bot

Before you start, understand how billing works for Microsoft’s cloud services. There are generally free tiers for selections, but they are resource limited. When you first start with the Azure magic cloudy stuff, you get a 200$ credit. A message indicating your remaining credit is shown when you log into the Azure portal. Pay attention to that message – if you think you are using free tiers for everything but see your credit decreasing …you’ll need to investigate. Some features, like usage analytics, cost extra too.

Microsoft Teams uses Azure bots – so you’ll need to create an Azure bot to get started. From https://portal.azure.com, click on ‘Create a resource’. Search for “bot” and find the bots you are looking for. To host your bot on Azure, select either the “Functions Bot’ or “Web App Bot”. Functions bots use Azure functions, which are C# scripts, for logic processing; WebApp bots use WebAPI App Service for logic processing (C# or NodeJS). To host your bot elsewhere, select “Bot Channels Registration”. In this example, we are using a “Web App Bot”.

Give your bot a name– there will be a green check if the name is unique. Pick your language – C# or Node.JS – and then decide if you want an Echo bot (which gives you a starting place if you’re new to developing bots) or a blank slate (basic bot). Don’t forget to click “Select” otherwise you’ll be back to the defaults. You’ll need to create a resource group. Click on “Bot template” and select what you want to use as the basis for your bot. As of 14 Dec 2018, use v3 unless you need something new in v4 – there’s a lot more available there, and the Bot Builder extensions only work with v3 (https://github.com/OfficeDev/BotBuilder-MicrosoftTeams)

You may need to create a service plan

And storage configuration. Once you have completed the bot configuration, click “Create” and Azure will deploy resources for your bot.

You’ll see a deployment process message, and your messages will have a similar notification. Wait a minute or three.

Return to the dashboard & you’ll see your bot services. Go into the bot that you just created.

Select “Build” – you can use the online code editor or use an existing source repository and configure a continuous integration. I will be setting up a continuous integration – don’t click the link under “Publish”, it goes to an old resource. Click to download the source code – it takes a minute to generate a zip file for download.

Once the download link is available, download and extract the file – this will be the base of your project. Put it somewhere – in this example, I’ll be using a GitHub project. Extract the zip file and get the content into your source repository. 

Return to your dashboard and open the App Service for your bot. Select the “Deployment Center”.

Select the appropriate source repository. When GitHub is used, you will need to sign in and grant access for Azure to use your GitHub account. Click “Continue” once the repository has been set up.

Select the build provider – Kudu or Azure Pipelines. Which one – that’s a personal preference. Azure Pipelines can deploy code stored in git (at least GitHub, never tried other Git services). Kudu can build code housed in Azure DevOps. Kudu has a debugging console that I find useful, and I’ve successfully linked Kudu up with GitLab to manage the build process elsewhere. Azure Pipelines is integrated with the rest of the Azure DevOps (hosted TFS) stuff, which is an obvious advantage to anyone already using Azure DevOps. It uses WebDeploy to deploy artifacts to your Azure websites (again, an advantage to anyone already doing this elsewhere).

The two build environments can be different – MS doesn’t concurrently update SDK’s in each environment, so there can be version differences. It’s possible to have a build fail in one that works in the other. Settings defined in one platform don’t have any meaning if you switch to the other platform (i.e. you’ll be moving app settings into a Build Definition file if you want to switch from Kudu to Azure Pipelines) so it’s not always super quick to swing over to the other build provider, but it might be an option.

I prefer Kudu, so I’ll be using it here.

Select your repository name from the drop-down, then select the project and branch you want to use for deployment. In my repository, the master branch has functional code and there is a working branch for making and testing changes.

Review the summary and click “Finish”.

In GitHub, you confirm a webhook has been added to your project on push events. From your project’s settings tab, select “Webhooks” and look for a azurewebsites URL that includes your bot name. You can view the results of these webhook calls by clicking “Edit” and scrolling down to “Recent deliveries”.

Add the interactions you want – information needs to be accessible from the Azure network, otherwise your bot won’t be able to get there. You can test your bot from the Azure portal to identify anything that works fine from your local computer but fails from the cloud. From the Web App Bot (*note* we are no longer in the App Service on the Azure portal — you need to select the bot resource), select “Test in Web Chat” and interact with your bot.

Once you have your bot working, you need to add the Teams channel to allow the bot to be used from Teams. Select “Channels” and click on the Teams logo.

There’s not much to set up for a bot – messaging is enabled by default. I don’t want IVR or real-time media functionality … but if you do click on the “Calling” tab. The “Publish” tab is to publish your bot in the Windows store – this might be a consideration, for instance, if you wanted to create a customer service interaction bot that enterprise customers could add to their Teams spaces (i.e. something you want random people to find and use). Since I am answering employee specific questions, I do not want to publish this bot to the Internet. Click “Save” when you have configured the channel as needed (in my case, just click ‘save’ without doing anything).

Review the publication terms and privacy statement. If these are agreeable, click “Agree”.

You’ll be returned to the Channels overview. Click on the hyperlinked “Microsoft Teams” – this will open a new URL that is your bot.

You can copy the URL here – others can use the same URL to use your bot. Either open the link in the Teams app

Or cancel and click “Use the web app instead” at the bottom of the screen.

Wait for it … your bot is alive!

That’s great … how do I interact with company resources? Quick answer is “you don’t” – this bot uses resources available on the Internet. To interact with private sources, the magic cloudy Microsoft network must be able to get there. Personally, I’d host my own bot engine. Expose the bot to the Internet and create a “Bot Channels Registration” instead.

Summary of MS Teams Card Support

Full card reference can be found at https://docs.microsoft.com/en-us/microsoftteams/platform/concepts/cards/cards-reference. Below is a summary of where various card types are supported in Microsoft Teams

Card TypeBots in TeamsMessaging X10nsConnectorsBot Framework
Adaptive 
Hero 
List 
O365 Connector 
Receipt 
Sign-in 
Thumbnail 

Did you know … you can post data to Teams channels from your own code?

This post contains more niche “for developers” information than most of my Teams info series 😊

You’ve seen third-party services with connectors in Teams – both services with connectors published to Teams and services that can use the “Incoming Webhook” connector to post data. Youcan use the “Incoming Webhook” connector within your code too. If you want to allow others to post information into their Teams spaces, you’ll need a configuration option that allows end-users to supply the webhook URL. If you want to post information into your Teams space, then you can include it directly in your code. If you are doing the former, provide something like the “Creating a Teams Webhook URL” instructions below to your end users. In this example, I am doing the later. The example script is Python code that gathers statistics and posts a summary to my Teams space.

Creating a Teams Webhook URL:

Create a URL for your incoming webhook – from the not-quite-a hamburger menu next to the channel into which you want to publish your data, select “Connectors”, find “Incoming Webhook”, and click “Add” (if you have already configured a webhook in your Teams space, you will see “Configure”instead, and will not have to ‘install’ the webhook)

Read the privacy policy and terms of use, and if they are acceptable click “Install”

Provide a name for your webhook – this name will be displayed on all posts made via this webhook. Scroll down.

You can customize the logo associated with your webhook posts – nice if your application has a well-known logo. Click create to generate the webhook URL.

Copy the URL somewhere, then click ‘Done’.

Using Teams Webhooks Within Code:

OK, now you’ve got something like this in a text document (no, I didn’t post a real webhook to the Internet – the long pseudo-random alphanumeric string is hex, and there aren’t a whole lot of m’s and q’s in hex!):

https://outlook.office.com/webhook/bge9922d-44fa-4c60-bp59-e935554ff4cd@2567m4c1-b0ep-40f5-aee3-58d7c5f3e2b2/IncomingWebhook/644915ga291e4ee499f2479a32qde691/9e0c4d6c-65d9-4f94-b0d5-4fbbb0238358

What do you do with it? You make a POST call to that URL and supply JSON-formatted card content in the data. Microsoft provides a complete card reference, but you’ll need to use the O365Connector Card with the incoming webhook connector.

The card requires “summary” or “text” be included – you’ll get a bad data HTTP response if you fail to set one of these values. Card text can be formatted in markdown or HTML – if you want to use HTML, you need to set markdown to false.

You’ll need a function that POSTs data to a URL:

################################################################################
# This function POSTs to a URL
# Requirements: requests
# Input: strURL -- URL to which data is posted
#        strBody -- content to be sent as data
#        strContentType -- Content-Type definition
# Output: BOOL -- TRUE on 200 HTTP code, FALSE on other HTTP response
################################################################################
def postDataToURL(strURL, strBody, strContentType):
    if strURL is None:
        print("POST failed -- no URL provided")
        return False
    print("Sending POST request to strURL=%s" % strURL)
    print("Body: %s" % strBody)
    try:
        dictHeaders = {'Content-Type': strContentType}
        res = requests.post(strURL, headers=dictHeaders,data=strBody)
        print(res.text)
        if 200 <= res.status_code < 300:
            print("Receiver responded with HTTP status=%d" % res.status_code)
            return True
        else:
            print("POST failed -- receiver responded with HTTP status=%d" % res.status_code)
            return False
    except ValueError as e:
        print("POST failed -- Invalid URL: %s" % e)
    return False

And you’ll need something to create a card and call the HTTP POST function. Here, I define a function that takes statistics on Windstream’s Microsoft Teams usage, formats a card with the values, and POSTs it to my webhook URL.

################################################################################
# This function posts usage stats to Teams via webhook
# Requirements: json
# Input: strURL -- webhook url
#        iPrivateMessages, iTeamMessages, iCalls, iMeetings -- integer usage stats
#        strReportDate - datetime date for stats
# Output: BOOL -- TRUE on 200 HTTP code, FALSE on other HTTP response
################################################################################
def postStatsToTeams(strURL,iPrivateMessages,iTeamMessages,iCalls,iMeetings,strReportDate):
    try:
        strCardContent = '{"title": "Teams Usage Statistics","sections": [{"activityTitle": "Usage report for ' + yesterday.strftime('%Y-%m-%d') + '"}, {"title": "Details","facts": [{"name": "Private messages","value": "' + str(iPrivateMessages) + '"}, {"name": "Team messages","value": "' + str(iTeamMessages) + '"}, {"name": "Calls ","value": "' + str(iCalls) + '"}, {"name": "Meetings","value": "' + str(iMeetings) + '"}]}],"summary": "Teams Usage Statistics","potentialAction": [{"name": "View web report","target": ["https://csgdirsvcs.windstream.com:1977/o365Stats/msTeams.php"],"@context": "http://schema.org","@type": "ViewAction"}]}'
        jsonPostData = json.loads(strCardContent)

        if postDataToURL(strURL, json.dumps(jsonPostData),'application/json'):
            print("POST successful")
            return True
        else:
            print("POST failed")
            return False
    except Exception as e:
        print("ERROR Unexpected error: %s" % e)
    return False

Run the program, and you’ll see information in Teams:

A personal recommendation based on my experience with third-party code that uses generic incoming webhooks — have a mechanism to see more than a generic error when the POST fails. It takes a lot of effort to pull apart what is actually being sent, turn it into a curl command to reproduce the event, and read the actual error.Providing a debug facility that includes both the POST body and actual response from the HTTP call saves you a lot of time should your posts fail.

Skype for Business & Teams Interoperation

Prior to August, when someone in Skype sent a message it showed up in my Teams client. And when I sent someone who had never used Teams a message, it showed up in their S4B client. Which was *exactly* the way I wanted it to work. And then Microsoft rolled some … enhancements. Now there’s an Island mode where messages are delivered to whatever platform originated them. Or a TeamsOnly mode when you’re done. Or a SkypeOnly mode when you’re not using Teams yet. And they’re working on some intermediary modes that will let you use Skype only but Teams for some limited subset of functions. I want what I had before their change!

And I finally figured out how to do it. It’s ugly. And eliminates the nice ad hoc “go try out Teams and use it whenever you want”. In addition to the tenant-level mode, there’s a user-level identity. Set anyone who wants to be a teams user to UpgradeToTeams.

PS O:\> Grant-CsTeamsUpgradePolicy -PolicyName UpgradeToTeams -Identity T05826@example.com
WARNING: Users with this policy will become full Teams-only users. They will no longer be able to use Skype for Business clients, except to join Skype for Business meetings. For details, see  http://aka.ms/UpgradeToTeamsPS

Messages sent from both Teams and Skype users to them will appear in their Teams client. Which is great; but, with the tenant in Island mode, they’ll still find themselves needing to log into Skype to talk to someone who is over there. You might be able to change the tenant to SkypeOnly (or the inverse, change the tenant to TeamsOnly and enumerate specific S4B users) but I don’t have a test tenant to, well, test that. But I can set anyone who isn’ta dedicated Teams user to be a SkypeOnly user.

O:\> Grant-CsTeamsUpgradePolicy -PolicyName SfBOnly -Identity T03826@example.com

And now T03826 will always get messages in Skype — even if e05826 sends them from Teams. And e05826 will always get messages in Teams — even if e03826 sends them from Skype. Amazing — I’ve managed to get back to where I was in July!

If the SkypeOnly user logs into Teams, they seem to be able to do anything they want … even sending messages to others in Teams. For the Teams recipient, there will be two listings for the SkypeOnly user. One with a Skype logo which will deliver messages to their Skype client (which also happens if you start a new chat and enter their name). Another, without the Skype logo, will deliver messages to their Teams client. Which they might miss if they’re not using Teams regularly.

Did you know … Microsoft Teams has a Twitter connector?

Microsoft publishes outage notifications several places –there’s a web portal that provides general status for major Office 365 outages,a customer-only portal that provides a LOT of details on every diminished service state, a Twitter feed with status on wide-spread outages. The common feature of all of these, though, is *checking* to see if there’s an outage –start seeing several calls for the same problem, check the outage site. It works, but it is not proactive.

Twitter could be proactive, but I don’t want to be diving for my phone every time a friend posts some random musing. But Microsoft Teams connects to Twitter, and it posts filtered content into a channel,so I only see pertinent information in our Teams channel. The connector can also search hash tags – useful for tracking brand mentions.

You can include Twitter posts into an existing channel or create a new one. I am creating a new channel to separate Twitter posts from other conversations.

Click the not-quite-a hamburger menu next to the channel into which you want Twitter posts to appear and select “Connectors”.

Locate “Twitter” and click “Configure”

You will need to configure a Twitter account before you can configure the connector. I recommend not using your personal account. Create a new account to be used by your Teams space. If ownership of the Teams space changes, the new owner can take over the Twitter account too. Click “Log in”

Click “Continue” to proceed with Twitter authentication

You are allowing the Twitter connector to use your account – read the access request and sign in if the usage is acceptable.

Once you have authenticated to Twitter, you will be able to configure the Teams connector. Enter accounts or tags to see Teams posts for relevant activity – do not put spaces after the commas!

Decide if you want Teams posts for replies, mentions, or re-tweets. Select the frequency with which you want the Teams connector to check for updates – our Microsoft outage notification connector is set to “Deliver individual messages as new tweets arrive”, but I configure most other connectors to create a digest every 1 or 6 hours. Once you have completed the connector configuration, click “Save”.

In your selected channel, you will find a post summarizing the connector configuration.

Did you know … you can share files in Microsoft Teams chats?

You can share files in both individual and group chats. In the chat, click on “Files”

Then click on “Share” and “Upload from my computer” to share a new file.

While it is uploading, you’ll see a status indicator.

And once the upload completes, you will get a desktop notification.

Voila – a shared file.

Where is that file? It’s been uploaded to your OneDrive for Business. Open the OneDrive web app and you’ll see a folder titled “Microsoft Teams Chat Files”.

In there, you’ll see all of the files you’ve shared in Teams. Click on “Shared”

And you can see with whom the file was shared.

If you shared a file in a group chat, everyone in the chat will be added to the file permission. If you shared a file in an individual chat, just that individual will be added.

If you want to share the same file with a second person, there’s no need to upload it again … doing so would remove the original permissions. When you see a warning that the file already exists, cancel.

Instead of uploading the file from your computer, select “OneDrive” from the “Share” menu.

Navigate into the “Microsoft Teams Chat Files” folder.

And select the file you wanted to share.

The new person with whom you want to share the file is added to the share list, and both individuals will see the file in the “Files” section of your Teams chat.

Be careful — deleting the file from your OneDrive space does not remove it from the Teams files list. The chat participants will get an error when they attempt to view the file.

Microsoft Graph — Application Registration

The application I am registering will pull report data from Graph for use within existing company systems. I will be assigning application-level permissions and no callback URL is needed.

To register an application, log into http://portal.azure.com and select “Azure Active Directory” from the left-hand navigation column. Then select “App Registrations (preview)”.

Click on “New registration”

Provide a descriptive name for the application — tenant managers can see all of the registered applications and it’s a lot easier if you ask them to approve access for “Specific Application Name For Engineering” than “LJR Test”.

The application will be created and you will be brought to the app overview. Select “API permissions” then click the “Microsoft Graph” hyperlink.

Click on “Application permissions”

And find the permissions you need. For the script I want to run, I need Reports.Read.All. Click “Update permissions” to save your changes.

If you are a tenant admin, you can approve your own rights. Otherwise, you’ll need to contact a tenant admin and have them approve the permissions you have requested. Once the permissions have been acknowledged, you’re ready to go.

You will need the app ID and a secret for use within your code. The application ID is listed on the application “Overview”.

To create a secret, select “Certificates & secrets” then click “New client secret”. This is displayed one time, so copy it into your code now.

Splunk Teams Connector – Followup

We managed to use the stock Teams webhook app in Splunk — just needed to modify the search being used. Adding “|table” and specific fields to be included in the table avoids having to filter the list data within the Python code

There still is a tweak to the code that I prefer — Python lists aren’t in any particular order. I’d like to be able to look the same place in the Teams post to see a particular field. Adding a sort when the facts array is put into the post body ensures the fields are in the same order each time.

        sections=[
            {"activityTitle": settings.get("search_name") + " was triggered"},
            {
                "title": "Details",
                "facts": sorted(facts)
            }
        ],

And I’ve got a Teams post from Splunk with a generic script — desired fields are specified within the search, so can be easily changed.

Splunk – Posting to Microsoft Teams via Webhooks

Using either the default webhook action or the Teams-specific webhook, Splunk searches can post data into Microsoft Teams. First, you need to get a webhook URL for your Teams channel. On the hamburger menu next to the channel, select “Connectors”. Select Webhook, provide a name for the webhook, and copy the webhook URL.

If you intend to use the generic webhook app, there is no need to install the Teams-specific one. The Teams-specific app gives you prettier output & a “view in splunk” button. Download the app tgz. To install the app, go into “Manage Apps” and select “Install app from file”.

Click ‘Browse…’ and find the tgz you downloaded. Click ‘Upload’ to install the app to Splunk.

Now create a search for which you want to post data into your Teams channel. Click “Save As” and select “Alert”

Provide a title for the alert — you can use real-time or scheduled alerts. Once you’ve got the alert sorted, select “Add Actions” and select the Teams webhook action (or the generic webhook action if you intend to use that one). Paste in the URL from your Teams channel webhook and click “Save”.

You”ll see a confirmation that the alert has been saved. Close this.

Now you would think you’d be ready to use it … but wait. Neither one works out of the box. In the Splunk log, you see error 400 “Bad data” reported.

For the default webhook app, edit the Python script (/opt/splunk/etc/apps/alert_webhook/bin/webhook.py in my case). Find the section where the JSON body is built. Teams requires a summary or title within the POST data. I just added a static summary, but you could do something fancier.

        body = OrderedDict(
            sid=settings.get('sid'),
            summary='LJRWebhook',
            search_name=settings.get('search_name'),
            app=settings.get('app'),
            owner=settings.get('owner'),
            results_link=settings.get('results_link'),
            result=settings.get('result')

For the Teams-specific webhook, edit the Python script (/opt/splunk/etc/apps/alert_msteams/bin/teams.py in my case) and find the section where the facts list is populated. There’s too much data being sent through. There’s probably a way to filter it out in Splunk, but I don’t know how 🙂

The right way to do it is select the most important items from settings.get(‘result’).items that you want to be displayed within Teams and populate facts with those elements. I used a new list, strWantedKeys, to determine which keys should be added to the facts list. The quick/ugly way is to just take the first n items from the result items (settings.get(‘results’).items()[:7] gets seven … 8 produced a ‘bad payload received by generic incoming webhook’ error from Teams.

try:
settings = json.loads(sys.stdin.read())
print >> sys.stderr, "DEBUG Settings: %s" % settings
url = settings['configuration'].get('url')
facts = []
strWantedKeys = ['sourcetype', '_raw', 'host', 'source']
for key,value in settings.get('result').items():
if key in strWantedKeys:
facts.append({"name":key, "value":value})
body = OrderedDict(

For reference, the original facts list was:

    "facts": [{
        "name": "index",
        "value": "history"
    }, {
        "name": "_raw",
        "value": "Test push to teams 555"
    }, {
        "name": "_eventtype_color",
        "value": ""
    }, {
        "name": "host",
        "value": "10.10.15.134:8088"
    }, {
        "name": "source",
        "value": "http:Sendmail testing"
    }, {
        "name": "_si",
        "value": ["49cgc3e5e52e", "history"]
    }, {
        "name": "sourcetype",
        "value": "mysourcetype"
    }, {
        "name": "_indextime",
        "value": "1544554125"
    }, {
        "name": "punct",
        "value": "___"
    }, {
        "name": "linecount",
        "value": ""
    }, {
        "name": "_time",
        "value": "1544554125"
    }, {
        "name": "eventtype",
        "value": ""
    }, {
        "name": "_sourcetype",
        "value": "mysourcetype"
    }, {
        "name": "_kv",
        "value": "1"
    }, {
        "name": "_serial",
        "value": "15"
    }, {
        "name": "_confstr",
        "value": "source::http:Sendmail testing|host::10.10.15.134:8088|mysourcetype"
    }, {
        "name": "splunk_server",
        "value": ""
    }]

Now generate a message that matches your search — you’ll see a post created in your Teams channel.

Did you know … you can record Microsoft Teams meetings (and add a transcription)?

Once you have started a Microsoft Teams meeting, click the not-quite-a-hamburger menu in the meeting control and select “Start recording.

You will see a confirmation that your meeting is recording – and, as the message mentions, make sure everyone knows they are being recorded.

If you are attending the meeting using a web browser, the browser tab will have a little red circle indicating that recording is active.

If you no longer wish to record the meeting, click the not-quite-a-hamburger-menu button again and select “Stop recording.

Now where is that recording?? Open the meeting chat

And you’ll see a post indicating that the recording is saving. Once the recording has been saved, a link to the recording will appear in the conversation. You can get a link to share with others.

If you would like a transcription to be created for your meeting, select “Open in Microsoft Stream”. This will open the recording in a web browser. Under the meeting information, click the not-quite-a-hamburger menu and select “Edit”

On the edit page, select the video language. Ensure “Captions” is checked.

Scroll up to the top of the page and click “Apply”.

Wait for it – the transcription process can take a loooong time. Once the transcription is done, you can click the gear icon in the video information and select “Show transcript” (if the transcript isn’t done yet, this option is still available. But the transcript pane will just tell you to come back later)

Read through the transcript – an AI engine is used to perform the natural language processing, and the transcription accuracy *should* improve as you use the service. Click on “Edit”, make any corrections

Click “Done” to save your changes.

When your video is played, there is now a button to display closed captioning.

And the transcript will be displayed as the video progresses.

When you view the transcript, you can copy the entire thing & paste the transcript into OneNote or the meeting chat to have computer-generated meeting notes. I usually edit the transcript to remove the timestamps (and remove any tangential discussions).