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)
Let’s write the endpoint, using the Dark editor autocomplete feature.
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
- From the bottom left corner of Teams, open the app list
- Look for App Studio, install an open it. In the manifest editor tab, you should see the button to create a new app
- Fill out the information, and generate an app ID
- 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
- There, set up a new endpoint, here
/api/message
- Now under the messaging extensions, select your bot already created
The endpoint should be the same as the bot. Generate a password on this screen too, and copy it somewhere
- In the test and distribute tab, you must fix all manifest error messages, then install the app at a team level to test it
- If the installation works, you should see the personal chat screen, let’s write a test message
- No response, but in the Dark canvas, a new 404 appears
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
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.
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:
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.
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.
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:
And here’s the actual code in Dark:
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.
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:
… And let’s run it!
This is it!
Let’s finish on a pic of our canvas