Building a Microsoft Teams App with AWS Lambdas

Nov 16, 2021 | By Alvin Cheng

If you haven't heard, today is a big day for the FloQast team. Today, we're proud to officially announce our much-anticipated integration with Microsoft Teams! This integration allows users to interact with FloQast directly from within Microsoft Teams, and we're absolutely thrilled to share it with the world.

If you're interested in learning more about the FloQast Microsoft Teams integration, be sure to check it out in the MS Teams store.

Today, we're here to take a look at all the technical work that took place to get to where we are now. Although the process of building a Teams app is very well documented and has many examples, we did run into several challenges along the way. Many of these stemmed from the fact that we built it as a serverless app using AWS Lambda, and that is what I will be talking about today.

Background

At FloQast, our amazing DevOps team has made it super easy for developers to create a brand new application. All we need to do is enter a simple slack command, and like magic, a repo is created with the back-end infrastructure already set up.

Magic

 

What makes this magic possible is the fact that we build serverless applications using AWS lambda. Each Slack command creates the underlying infrastructure for a brand new lambda. Within seconds, we were able to create the lambda that would house the code for our Teams app.

The challenge here was that all the documentation and examples revolve around building the Teams app using a RESTful API, with frameworks such as Express or Restify. A quick Google search for “Microsoft Teams app on AWS Lambda” or “Microsoft Teams App Serverless” provided very few results.

Tumbleweed

 

As a result, we had to do a lot of trial and error and documented a lot of the technical roadblocks and solutions along the way.

Challenge 1: Initial Setup

When registering a Teams bot, you are required to provide an endpoint. This endpoint will receive POST requests whenever a user interacts with the bot in Teams. Fortunately, we were able to use an ALB to set the HTTP endpoint which is used to call the lambda.

Challenge 2: The Response Object

In virtually every one of the botbuilder samples, you see the bot instantiated with this block of code:

server.post('/api/messages', (req, res) => {
  adapter.processActivity(req, res, async (turnContext) => {
    await bot.run(turnContext);
  });
});

The docs specify that they expect an “Express or Restify style request and response object” here. Unfortunately, the lambda isn't invoked with these objects. To get around this, we had to get a little creative.

Creativity

 

We mocked our own request and response objects. The request object was pretty simple - we used http.IncomingMessage and attached the event headers and event body to build the request object.

const { IncomingMessage } = require('http');

exports.handler = async (event) => {
  const req = Object.assign(new IncomingMessage(), {
    headers: event.headers,
    body: event.body
  });

  const res = new Response();

  await adapter.processActivity(req, res, async (turnContext) => {
    await bot.run(turnContext);
  });
  return res.end();
};

The response object was a bit trickier. The first thing we had to do was ensure it had to correct methods on it.

class Response {
  send(statusCode, body) {
    this.statusCode = statusCode;
    this.body = body;
    return {
      statusCode: statusCode || 200,
      body: body || ''
    };
  }
  status(statusCode) {
    this.statusCode = statusCode;
    return {
      json(body) {
        return {
          statusCode: statusCode || 200,
          body: JSON.stringify(body || '')
        };
      }
    };
  }
  set(headers) {
    this.headers = headers;
  }

  write(body) {
    this.body = body;
  }

  end() {
    const headers = this.headers || {};
    return {
      statusCode: this.statusCode || 200,
      headers,
      body: this.body || ''
    };
  }
}

Next, we also had to ensure that we were returning the correct body for the different types of actions. One good example our implementation of a modal experience, or what they call task modules. The docs show that you simply need to return the task:

// from the docs
handleTeamsTaskModuleFetch(context, taskModuleRequest) {
  const { data } = taskModuleRequest;
  return {
    task: {
      type: 'continue',
      value: {
        card: buildCard(data)
      }
    }
  };
}

We had to get a bit creative in our case. We first pass in the response object through the turnContext. Then, in the bot, we manually mutate the object to have the correct body (mimicking res.status(200).json({ task })). Lastly, when we return res.end(), we ensure the lambda is responding with the correct body that Teams expects.

// In our lambda

// index.js
await adapter.processActivity(req, res, async (turnContext) => {
  turnContext.res = res;
  await bot.run(turnContext);
});
return res.end();

// bot.js
handleTeamsTaskModuleFetch(turnContext, taskModuleRequest) {
  const { data } = taskModuleRequest;
  const card = buildCard(data);
  const body = {
    task: {
      type: 'continue',
      value: {
        card
      }
    }
  };
  turnContext.res.set({
    'Content-Type': 'application/json',
  });
  turnContext.res.write(JSON.stringify(body));
}

Challenge 3: Keeping Code DRY

One of the main functionalities of our app is that we send several different notifications to users. Our philosophy is to scale out instead of up, meaning we have a different lambda for each notification, which we fire per user. There is a lot of overhead set up in order to send a proactive message through teams. To keep the lambdas lightweight as well as the code DRY, we created a separate lambda which is solely responsible for sending Teams messages. In this lambda, we also included Adaptive Card templates, which allow us to maintain a consistent look and feel throughout our app.

Summary

Building our Microsoft Teams integration as a serverless app with AWS Lambda was a difficult but rewarding experience. Through trial and error, we ran into and overcame many different obstacles, which we documented along the way. Hopefully, this can help anyone who is following a similar path. Microsoft Teams is a super powerful tool and we've only scratched the surface of all the possible features that we can build, so I look forward to tackling more of these challenges in the future!

Alvin Cheng
Alvin is a software engineer at FloQast. He is on a never-ending quest to discover the best fried chicken in the world.

Check out research, videos, case studies, and more!

Learn more about working at FloQast!

X

Schedule a Personalized Demo