Tutorial: writing a Microsoft Teams Bot (an introduction to Dark)

This tutorial is relevant to write bots with functions and other backends beside Dark.

Lexicon

Dark

Also known as Darklang (the SEO term), it's a deployless backend with its own functional programming language and editor.

Introduction

In the US, Slack reigns supreme, but in Europe, Teams and other Microsoft products are used a lot among development teams.

And unlike Slack for which integrations are pretty easy and well documented, it is hard to code something for Teams, even more so if you don't want to use their overly abstracted Botframework library.

So there we go, using good ol' REST API to write a bot!

Hello World

To get familiar with Dark very quick, let's first write a Hello World endpoint.

You need to have a Dark canvas to follow along.

Dark uses a sound type system, so we'll take advantage of that. Let's first write something that breaks, like in Test Driven Development. My Canvas is https://darklang.com/a/onepoint-onebot, so my app will be https://onepoint-onebot.builtwithdark.com/, and I'll ping a first endpoint called /test.

❯ curl https://onepoint-onebot.builtwithdark.com/test  
404 Not Found: No route matches

Of course it doesn't exist, but the important detail is that now in my canvas, in the 404s dropdown, I now have my endpoint with the HTTP method used (GET), and a + sign to add it! (if it sounds awesome, it's because it is)

404

Let's write the endpoint, using the Dark editor autocomplete feature.

Hello World!

And now, the same HTTP request gives us:

❯ curl https://onepoint-onebot.builtwithdark.com/test  
{ "message": "Hello World!" }

Creating the Bot App

Now we need an app to talk to on Teams, and for the backend, an app ID and secret key.

So now on Teams

  1. From the bottom left corner of Teams, open the app list

App list

  1. Look for App Studio, install an open it. In the manifest editor tab, you should see the button to create a new app

Create app

  1. Fill out the information, and generate an app ID

App information

  1. In the bot tab under capabilities, set up a bot with the scope you want. We are going to allow personal, teams and group chats

Bot configuration

Bot scope

  1. There, set up a new endpoint, here /api/message

Bot configuration

  1. Now under the messaging extensions, select your bot already created

Bot messaging

The endpoint should be the same as the bot. Generate a password on this screen too, and copy it somewhere

Bot messaging

  1. In the test and distribute tab, you must fix all manifest error messages, then install the app at a team level to test it

Bot test

  1. If the installation works, you should see the personal chat screen, let's write a test message

Bot chat

  1. No response, but in the Dark canvas, a new 404 appears

Bot 404

Send a Message Back

By creating the endpoint on Dark, we can see the data of the request that was sent to it, with the dots on the left side of the canvas item

Endpoint log

By returning request.jsonBody, we can have a better view of what was sent to the endpoint

200 {  
  Content-Type: "application/json"  
}  
{  
  channelData: {  
    tenant: {  
      id: "10f03bd7-bce6-4fe5-9577-b345ca86ac69"  
    }  
  },  
  channelId: "msteams",  
  conversation: {  
    conversationType: "personal",  
    id: "a:1gCjtfFAdfeJf0L2t5E4kk_fPBQAW_OoeZkrgoXRi1AXmPJ5FtSYbW5jWmi4_-pysxVZX9tBwuA3UKM6fX83-DABrteDIfXrkrSL4mV_3xy-LyF6gJ-GQG_cRClblAF6A",  
    tenantId: "10f03bd7-bce6-4fe5-9577-b345ca86ac69"  
  },  
  from: {  
    aadObjectId: "cff3ce1d-ba46-4d26-931c-95e70a05b9b8",  
    id: "29:1qVcpzNNZYUJwMFaDyBP4bN3lewp7rZGT7LDdHi_eSh_e2W92qm51bsZT4ET3_K2Uut4K98EvJC-X05NyzI4xuw"  
  },  
  id: "f:638309e7-ba0e-e0d5-10df-6692766db0e0",  
  membersAdded: [  
    {  
      aadObjectId: "cff3ce1d-ba46-4d26-931c-95e70a05b9b8",  
      id: "29:1qVcpzNNZYUJwMFaDyBP4bN3lewp7rZGT7LDdHi_eSh_e2W92qm51bsZT4ET3_K2Uut4K98EvJC-X05NyzI4xuw"  
    }  
    , {  
      id: "28:9d9cced4-edd3-4266-9479-02e4634bdc44"  
    }  
  ],  
  recipient: {  
    id: "28:9d9cced4-edd3-4266-9479-02e4634bdc44",  
    name: "Onebot"  
  },  
  serviceUrl: "https://smba.trafficmanager.net/emea/",  
  timestamp: "2020-03-25T20:40:12.0125067Z",  
  type: "conversationUpdate"  
}  

This is going to be helpful later.

Authentication

To be able to send a message to the user, we need the channel and converation data, but also to get an access token.

We'll take advantage of this opportunity to use other canvas items, like datastores, REPL and functions.

Storing the App ID and Password

Let's first create a datastore that we'll call Credentials, with an appId and appPassword, both of type String.

Credentials

Notice the green lock on the top left corner, thqt meqns thqt you cqn freely delete this datastore. Once they are populated with data, the lock turns red. To delete a populated datastore, you first need to write a REPL, function or something to delete all items in it, like the following:

DB::deleteAll Credentials  

Now the REPL to pupulate the datastore:

Populate

Each datastore item has a key, which makes it easy to fetch later on.
There you can see that it's dependant on the Credentials datastore.

You can run the command by clicking on the round arrow at the canvas item level, in the top right corner, or by clicking on the "play" triangle next to DB::set.

... And now it's locked and populated.

Populated

The Access Token

Based on the hard to find Bot Framework REST API documentation, to obtain the access token, we need to make a call of the following type:

POST
https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&client_id=MICROSOFT-APP-ID&client_secret=MICROSOFT-APP-PASSWORD&scope=https%3A%2F%2Fapi.botframework.com%2F.default

Writing a Function

Let's translate this into a Dark HTTP client request, in a function, so it can be called in multiple places, and don't use space on the main canvas, as functions are displayed in separate canvas views.:

Let's get the credentials we need, by key:

DB::get "teams" Credentials

... And here's the complete function, we translated the body query-type syntax for a dict.
The last evaluation is returned by the function without the need to use the return keyword, like in Ruby. We'll be able to call this function and get the access token from it.

Authentication function

Respond to the User

Based on the Bot Framework REST API documentation, to respond to a user, we need to send something like this:

POST  
https://smba.trafficmanager.net/apis/v3/conversations/abcd1234/activities/bf3cc9a2f5de...   
Authorization: Bearer eyJhbGciOiJIUzI1Ni...  
Content-Type: application/json  
  
{  
    "type": "message",  
    "from": {  
        "id": "12345678",  
        "name": "bot's name"  
    },  
    "conversation": {  
        "id": "abcd1234",  
        "name": "conversation's name"  
   },  
   "recipient": {  
        "id": "1234abcd",  
        "name": "user's name"  
    },  
    "text": "I have these times available:",  
    "replyToId": "bf3cc9a2f5de..."  
}  

As you can see, we need to pass the access token in the HTTP request heqder, qs Authorization: Bearer <TOKEN>. So it made sense to write that function before, we'll use it now.
We'll also need to grab the activity ID for the URL to send the request to, and the conversation ID for the request and also the JSON object we'll send in the request body, with the recipient ID too.

Let's send a message to the bot from the channel by mentioning it, we get this response:

Bot Response

And here's the actual code in Dark:

Response Endpoint

Conclusion

This was fairly simple! And if not, the tricky parts are definitely from Microsoft Teams, not Dark!

Also, this backend can be rewritten in NodeJS with functions, or any backend language and framework, the knowledge and the right data to send to the Bot Framework REST API endpoints is the same.

TheEnd

To Go Further

... This is a bonus!

Spontaneous Messages!

Responding to messages is fine, but what about sending messages out of nowhere? There are several use cases, a third party service could send a request to the Dark backend, and Dark would need to notify the Teams channel, for example.

Let's start with what we already have, modifying the data we send to the Bot Framework endpoint. This is not well documented in the Bot Framework documentation, by the way...

POST  
https://smba.trafficmanager.net/emea/v3/conversations  
Authorization: Bearer eyJhbGciOiJIUzI1Ni...  
Content-Type: application/json  
  
{  
    "bot": {  
        "id": "12345678",  
        "name": "bot's name"  
    },  
    "isGroup": true,  
    "topicName": "Topic 1",  
    "channellData": {  
        "channel": {  
            "id": "abcd1234",  
       }  
   },  
   "activity": {  
        "type": "message",  
        "text": "Message to Send"  
    }  
}  

As you can see, what we mostly need is the authentication as before, and a channel ID. Ideally, you'd grab it when the bot is invited, or make an init command to define the channel that the bot should use. Here, to not spend time on this, I'll use our previous endpoint to save the channel ID in a datastore.

There is the canvas function:

Message function

... And let's run it!

Teams bot message

This is it!

Let's finish on a pic of our canvas

Canvas

Glorious!