Search code examples
firebaseexpressgoogle-cloud-functionsmiddlewareslack-api

Use Firebase onRequest() or Express app.use() for the Slack API


Goal

Use the @slack/interactive-message package with firebase-functions to listen and respond to Slack messages and dialogs.

Question

I'm not sure how to use the @slack/interactive-message listener with firebase.

1) Do I use Firebase's functions.https.onRequest(), and somehow pass the req from Slack to slackInteractions.action()?

OR

2) Do I use app.use("/app", slackInteractions.expressMiddleware()); If so, where do slackInteractions.action()s go?

OR

3) Something else?

Code

// Express
import express = require("express");
const app = express();
const cors = require("cors")({
  origin: "*"
});
app.use("*", cors);

// Firebase Functions SDK
import functions = require("firebase-functions");

const slackbotConfig = functions.config().slackbot;
const { createMessageAdapter } = require("@slack/interactive-messages");
const slackInteractions = createMessageAdapter(slackbotConfig.signing_secret);

app.use("/app", slackInteractions.expressMiddleware());

// Express route
app.post("/go", (req, res) => {
  console.log("Hello from Express!");
  res
    .status(200)
    .send("Hello from Express!")
    .end();
});

exports.app = functions.https.onRequest(app);

exports.helloWorld = functions.https.onRequest((_req, res) => {
  console.log("Hello from Firebase!");
  res
    .status(200)
    .send("Hello from Firebase!")
    .end();
});

tl;dr

I'm new to the details of Express and using middleware. Examples of the @slack/interactive-message show...

slackInteractions.start(port).then(() => {
  console.log(`server listening on port ${port}`);
});

...and with Firebase Cloud Functions, this bit isn't relevant. I'm not sure how listeners, requests, and responses are integrated between Firebase and @slack/interactive-message


Solution

  • creator of @slack/interactive-messages here 👋

    In short, your solution number 2 seems correct to me. While I don't have experience with Firebase functions, I have a pretty good understanding of express, and I'll provide some more details.

    What is express middleware?

    Express middleware is a name for a kind of function that processes an incoming HTTP request. All middleware functions can, on a request-by-request basis, choose to pre-process a request (usually by adding a property to the req argument), respond to the request, or post-process a request (like calculate the timing between the request and the response). It can do any one or combination of those things, depending on what its trying to accomplish. An express app manages a stack of middleware. You can think of this as a list of steps a request might work through before a response is ready. Each step in that list can decide to offer the response so that the next step isn't even reached for that request.

    The cors value in your code example is a middleware function. It applies some rules about which origins your Firebase function should accept requests from. It applies those rules to incoming requests, and when the origin is not allowed, it will respond right away with an error. Otherwise, it allows the request to be handled by the next middleware in the stack.

    There's another middleware in your example, and that's a router. A router is just a kind of middleware that knows how to split an app up into separate handlers based on the path (part of the URL) in the incoming request. Every express app comes with a built in router, and you attached a handler to it using the app.post("/go", () => {}); line of code in your example. Routers are typically the last middleware in the stack. They do have a special feature that people often don't realize. What are these handlers for routes? They are just more middleware functions. So overall, you can think of routers as a type of middleware that helps you divide application behavior based on the path of a request.

    What does this mean for slackInteractions?

    You can think of the slackInteractions object in your code as a router that always handles the request - it never passes the request onto the next middleware in the stack. The key difference is that instead of dividing application behavior by the path of the request, it divides the behavior using the various properties of a Slack interaction. You describe which properties exactly you care about by passing in constraints to the .action() method. The only significant difference between a typical router and slackInteractions, is that the value itself is not the express middleware, you produce an express middleware by calling the .expressMiddleware() method. It's split up like this so that it can also work outside of an express app (that's when you might use the .start() method).

    Putting it together

    Like I said, I don't have experience with Firebase functions specifically, but here is what I believe you should start with as a minimum for a function that only handles Slack interactions.

    // Firebase Functions SDK
    import functions = require("firebase-functions");
    const slackbotConfig = functions.config().slackbot;
    
    // Slack Interactive Messages Adapter
    const { createMessageAdapter } = require("@slack/interactive-messages");
    const slackInteractions = createMessageAdapter(slackbotConfig.signing_secret);
    
    // Action handlers
    slackInteractions.action('welcome_agree_button', (payload, respond) => {
      // `payload` is an object that describes the interaction
      console.log(`The user ${payload.user.name} in team ${payload.team.domain} pressed a button`);
    
      // Your app does some asynchronous work using information in the payload
      setTimeout(() => {
        respond({ text: 'Thanks for accepting the code of conduct for our workspace' });
      }, 0)
    
      // Before the work completes, return a message object that is the same as the original but with
      // the interactive elements removed.
      const reply = payload.original_message;
      delete reply.attachments[0].actions;
      return reply;
    });
    
    // Express
    import express = require("express");
    const app = express();
    app.use("/", slackInteractions.expressMiddleware());
    
    exports.slackActions = functions.https.onRequest(app);