Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrating a Bolt app into another server or framework (upward modularity) #212

Closed
rrrepos opened this issue Jul 3, 2019 · 41 comments
Closed
Labels
auto-triage-stale discussion M-T: An issue where more input is needed to reach a decision

Comments

@rrrepos
Copy link

rrrepos commented Jul 3, 2019

I would like to use bolt as an express middleware similar to how the @slack/events-api work.

app.use('/slack/events', slackEvents.expressMiddleware());

Is there an equivalent in bolt? Thanks

@alisajidcs
Copy link

Any Progress on This issue?

@Startouf
Copy link

Startouf commented Oct 9, 2019

I would also be interested as a possible way to solve #283

@dcsan
Copy link

dcsan commented Nov 10, 2019

is this related to expressReceiver ?

@seratch seratch added discussion M-T: An issue where more input is needed to reach a decision enhancement M-T: A feature request for new functionality labels Jan 4, 2020
@regniblod
Copy link

Is there any update on this?

I'm trying to implement it in my NestJS application but neither NestJS or Bolt allows me to pass an existing express instance.

@llwor94
Copy link

llwor94 commented May 20, 2020

Same boat as @regniblod - trying to combine Nestjs and bolt. Any update on this?

@aoberoi
Copy link
Contributor

aoberoi commented Jun 10, 2020

Hi folks! Integrating Bolt for JS into other HTTP servers/frameworks is something we're interested in making happen. Within the team, we've called this idea "upward modularity" since its about making Bolt fit inside a larger app. (It's probably not important but "downward modularity" would be about combining several parts of a Bolt app into one Bolt app).

We want this to work in a generic way, so that Bolt can integrate not only into NestJS, but into nearly any web server/framework (Express, hapi, plain Node http servers, Koa, etc). In fact, there's some prior work to integrate Bolt into a Koa application by @barlock's team here.

The way to move this topic forward would be with a proposal. If you have a specific idea for how you think this should work, please go ahead and write up a description. It doesn't need to be anything formal, just something to describe how you'd like to see the feature work. We can help suss out any questions that arise from that, and the community can help design a solution.

PS. If you just want to hook into Bolt's underlying express app by adding a few custom routes, you can already do that, but we need to document that better.

@aoberoi aoberoi removed the enhancement M-T: A feature request for new functionality label Jun 10, 2020
@aoberoi aoberoi changed the title Using Bolt as Express Middleware Integrating a Bolt app into another server or framework (upward modularity) Jun 10, 2020
@duke79
Copy link

duke79 commented Jul 13, 2020

Meanwhile, this worked for me -

Extract the express app from bolt and add nestjs middleware.

import { App, ExpressReceiver } from '@slack/bolt';
import { AppMiddleware } from './nestj/app.middleware';

const receiver = new ExpressReceiver({ signingSecret: configuration.slackSigningSecret });

const app = new App({
      receiver,
      token: configuration.slackAccessToken,
      signingSecret: configuration.slackSigningSecret,
    });
    
    receiver.app.use((req, res, next) => {
      const nest = new AppMiddleware(app).use(req, res, next);
      nest.then(() => {
        next();
      }).catch(err => {
        next();
      });
    });

    // Start your app
    const port = configuration.port || 3000;
    await app.start(port);
    console.log("⚡️ Bolt app is running! on port " + port);

https://slack.dev/bolt-js/concepts#custom-routes


I followed this SO answer to implement the NestJS middleware.

app.middelware.ts

import { Injectable, NestMiddleware } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { ExpressAdapter } from '@nestjs/platform-express';
import { AppModule } from './app.module';

const bootstrap = async (express: Express.Application) => {
  const app = await NestFactory.create(AppModule, new ExpressAdapter(express));
  await app.init();
  return app;
}

@Injectable()
export class AppMiddleware implements NestMiddleware {

  constructor(private expressInstance: Express.Application) {}

  use(req: any, res: any, next: () => void) {
    console.log('In Nest middleware');
    return bootstrap(this.expressInstance);
  }
}

@seblaz
Copy link

seblaz commented Jul 19, 2020

Hi!
I think it would be great if we could do something like:

const { App } = require('@slack/bolt');
const express = require('express')
const expressApp = express()

const boltApp = new App({
      token: configuration.slackAccessToken,
      signingSecret: configuration.slackSigningSecret,
});

const boltMiddleware = boltApp.getMiddleware();
expressApp.use('/bolt', boltMiddleware);

@bertho-zero
Copy link

bertho-zero commented Sep 1, 2020

Works fine for me:

const { App } = require('@slack/bolt');
const express = require('express');

const app = express();

const boltApp = new App({
    signingSecret: config.slackApp.signingSecret,
    token: config.slackApp.token,
    endpoints = '/'
});

app.use('/slack/events', boltApp.receiver.router); // works also with boltApp.receiver.app

You can add a path in the app.use and modify the Bolt endpoint(s).

@cShingleton
Copy link

cShingleton commented Sep 3, 2020

To cover the Hapi framework v.17+ you can jerry-rig a solution by registering Bolt's provided Express Receiver as a plugin using the hecks package. It's not the most elegant solution but I needed something that worked quickly... Hope it helps someone!

const Hapi = require('@hapi/hapi');
const Hecks = require('hecks');
const { App, ExpressReceiver } = require('@slack/bolt');


// INIT BOLT APP
const Receiver = new ExpressReceiver({ signingSecret: SLACK_SIGNING_SECRET });

const BoltApp = new App({
  token: SLACK_BOT_TOKEN,
  signingSecret: SLACK_SIGNING_SECRET,
  receiver: Receiver
});


// INIT HAPI SERVER
const init = async () => {

    const server = Hapi.server({
        port: 3000,
        host: 'localhost'
    });

    await server.register([Hecks.toPlugin(BoltApp.receiver.app, 'my-bolt-app')]);

    server.route({
      method: '*',
      path: '/slack/events',
      handler: {
        express: BoltApp.receiver.app
      }
  });

    await server.start();
    console.log('Server running on %s', server.info.uri);
};

process.on('unhandledRejection', (err) => {
    console.log(err);
    process.exit(1);
});

init();

@t6adev
Copy link

t6adev commented Feb 6, 2021

Hi there!! Thank you for all of your advices.
My scenario is to combine multiple slack apps in a same server.
In TS + express, like this:
createApp.ts

import { App, ExpressReceiver } from '@slack/bolt';

export const createApp = (appName: string) => {
  const signingSecret = ...;
  const token = ...;
  const receiver = new ExpressReceiver({
    signingSecret,
    endpoints: {
      events: `/${appName}/slack/events`,
    },
  });

  const app = new App({
    token,
    receiver,
  });

  return { app, receiver };
};

one slack app: app-a.ts

import { createApp } from './createApp';

const { app, receiver } = createApp('app-a');

app.message('hello app-a', async ({ body, say }) => {
  await say(`Hey there, I'm app-a`);
});

export { receiver };

server.ts

import express from 'express';

import { receiver as appA } from './app-a';
import { receiver as appB } from './app-b';

const app = express();

// you can add more apps
app.use(appA.router); // App A's Event Subscription > Request URL is https://yourserver/app-a/slack/events
app.use(appB.router); // App B's Event Subscription > Request URL is https://yourserver/app-b/slack/events

app.listen(3030);

@Rieranthony
Copy link

Rieranthony commented Jun 16, 2021

receiver

Thanks @tell-y works like a charm 🙏

@jyb247
Copy link

jyb247 commented Jun 18, 2021

hi there!

context: @slack/bolt 3.4.0 + typescript

with a lot of inspiration from this thread and @tell-y code, I still can't get slack to connect to my public server; the ultimate objective is to get slashCommands working with bolt as it did using a custom solution.

maybe anyone of you see something obvious that is missing or mis-configured... ?

status:

  • a nacked postmap's POST to https://xxx/hooks/slack/events returns correctly a 401 unauthorized, as no bot/secret is passed, this confirms that the route is correctly setup (Logs indicate missing signing secret)
  • Slack App "Event Subscriptions" Configuration returns Your URL didn't respond with the value of the challenge parameter.
  • Slack App is configured with SlashCommands (/yep), Permissions (chat:write commands file:write), Bot (no Incoming, no Interactive, no EventsSubscription as it does not work)
  • the other way works fine; I have a bot, it's registered to a channel, I can post anything to it
  • Logs when slack is trying to confirm event url, shows the following:
{"level":50,"time":1624016171623,"pid":21,"hostname":"a1d27051-98ca-46f8-a4b7-34cf0402a708","message":"request aborted","code":"ECONNABORTED","expected":null,"length":null,"received":0,"type":"request.aborted","stack":"BadRequestError: request aborted\n    at IncomingMessage.onAborted (/app/node_modules/raw-body/index.js:231:10)\n    at IncomingMessage.emit (events.js:210:5)\n    at IncomingMessage.EventEmitter.emit (domain.js:475:20)\n    at abortIncoming (_http_server.js:492:9)\n    at socketOnEnd (_http_server.js:508:5)\n    at Socket.emit (events.js:215:7)\n    at Socket.EventEmitter.emit (domain.js:475:20)\n    at endReadableNT (_stream_readable.js:1184:12)\n    at processTicksAndRejections (internal/process/task_queues.js:80:21)","msg":[]}

main code bits:

file src/connectors/slack.ts:

import { ExpressReceiver, App, LogLevel } from '@slack/bolt';
const receiver = new ExpressReceiver({
    signingSecret: config.SLACK_SIGNING_SECRET,
    endpoints: '/',
});
export const app = new App({
    token: config.SLACK_BOT_TOKEN,
    receiver,
    socketMode: false,
    logLevel: LogLevel.DEBUG,
});
export const router = receiver.router;

file server.ts:

import * as slack from '@connectors/slack';
...
const app = express();
...
app.use('/hooks/slack/events', slack.router);
...
slack.app.message('hello', async ({ message, say }) => {
        await say(`Hey <@${message.channel}>!`);
});
slack.app.command('/yep', async ({ command, ack, say }) => {
        await ack();
        await say(`>>2> ${command.text}`);
});
...

@captDaylight
Copy link

captDaylight commented Jun 18, 2021

@bertho-zero @cShingleton @aoberoi When I try the approach with boltApp.receiver.router or boltApp.receiver.app I'm getting:

Property 'receiver' is private and only accessible within class 'App'

Does anyone have a working example of integrating Bolt into an existing express app? I'm struggling to find a clear migration example from the deprecated slack packages.

@jyb247
Copy link

jyb247 commented Jun 18, 2021

@captDaylight I had the same issue; the solution is in above code receiver = new Receiver(); + new App({..., receiver}) then use receiver.router

@captDaylight
Copy link

captDaylight commented Jun 18, 2021

Thanks @jyb247, when I consolidate your comment with another one from above I get something like this:

import { App, ExpressReceiver } from '@slack/bolt';
import express from 'express';

const app = express();

const receiver = new ExpressReceiver({ signingSecret: process.env.SLACK_SIGNING_SECRET });

const boltApp = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  receiver,
});

app.use('/', receiver.router);

EDIT: Removed error message, resolved issue

@jyb247
Copy link

jyb247 commented Jun 18, 2021

@captDaylight how does your package.json look like? (list only lines with slack)

@captDaylight
Copy link

captDaylight commented Jun 18, 2021

@jyb247 disregard, thanks for the help!

@jyb247
Copy link

jyb247 commented Jun 18, 2021

@captDaylight so I guess all is working on your side, incl events and slashCommands ? if so, could you share your Slack App Configuration (as to find a solution to my original solution)

@captDaylight
Copy link

@jyb247 I just have the basics done, but I'll post an update early next week once I've made some more progress.

@senguttuvang
Copy link

Hey guys, we have attempted to integrate Bolt with Express. Since the slack team works at bolt level, we thought of offering a higher-level abstraction, leveraging Express with simple structure and brought in Bolt. The idea is to build a starter kit for building Slack (and Teams app) apps without worrying about low-level details.

This is in the draft stage, ideas & suggestions are welcome!

https://github.com/PearlThoughts/LegoJS

@alexbaileyuk
Copy link

@jyb247 did you ever get a solution for the UnhandledPromiseRejectionWarning: BadRequestError: request aborted issue? I think I'm getting exactly the same problem as you are:

App listening on port 3000.
(node:187857) UnhandledPromiseRejectionWarning: BadRequestError: request aborted
    at IncomingMessage.onAborted (/home/alex/dev/personal/membr-bot/node_modules/raw-body/index.js:231:10)
    at IncomingMessage.emit (events.js:315:20)
    at abortIncoming (_http_server.js:561:9)
    at socketOnEnd (_http_server.js:577:5)
    at Socket.emit (events.js:327:22)
    at endReadableNT (internal/streams/readable.js:1327:12)
    at processTicksAndRejections (internal/process/task_queues.js:80:21)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:187857) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 1)
(node:187857) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

@8YOne
Copy link

8YOne commented Jul 7, 2021

@alexbaileyuk @jyb247
I came across the same issues as you mentioned (request aborted). Make sure when you hook the router to your express app, you do it before all the other middlewares that manipulate the body (ie. before json parsers, body parsers, etc).
The ExpressReceiver has its own built in body parser

@jamalavedra
Copy link

@jyb247 I just have the basics done, but I'll post an update early next week once I've made some more progress.

Did you figure it out with events and commands?

@alexbaileyuk
Copy link

@8YOne that solved it for me thanks!

@matthiassb
Copy link

Is there a simple working example for this scenario?

@recurrence
Copy link

I suspect this will resolve this issue ? #868

@rtrembecky
Copy link

I'm currently working on a rewrite from @slack/interactive-messages. I have an express app handling multiple bots at subpaths. Just coming here to thank you guys, because your suggestions worked. Here is a simple example of what works for me:

const app = express()

const boltReceiver = new ExpressReceiver({signingSecret, endpoints: '/'})
const boltApp = new App({token: botToken, receiver: boltReceiver})

boltApp.event('member_joined_channel', ({event}) => handleMemberJoined(event))
boltApp.event('message', ({event}) => handleMessage(event))

app.use(`/events/${botSubpath}`, boltReceiver.router)

Simple as that. Thanks again.

@billyvg
Copy link

billyvg commented Nov 13, 2021

Works fine for me:

const { App } = require('@slack/bolt');
const express = require('express');

const app = express();

const boltApp = new App({
    signingSecret: config.slackApp.signingSecret,
    token: config.slackApp.token,
    endpoints = '/'
});

app.use('/slack/events', boltApp.receiver.router); // works also with boltApp.receiver.app

You can add a path in the app.use and modify the Bolt endpoint(s).

I'm currently doing something like this except in typescript, it complains that receiver is private.

@rtrembecky
Copy link

@billyvg Hi, it was mentioned later in the thread that this is indeed an issue. Please check my last post for a clean solution in typescript.

@zaclittleberry
Copy link

@rtrembecky Thank you for coming back and updating your code examples!

I am trying to implement this, but I keep getting a type error on the boltReceiver.router passed to app.use as the second param.

No overload matches this call.
  The last overload gave the following error.
    Argument of type 'IRouter' is not assignable to parameter of type 'Application'.
      Type 'IRouter' is missing the following properties from type 'Application': init, defaultConfiguration, engine, set, and 30 more.ts(2769)
index.d.ts(48, 5): The last overload is declared here.

As far as I can tell, what I have is functionally the same as your code snippet and I'm left scratching my head.

Have you noticed/had to work around this type error at all? I'm using "@slack/bolt": "3.8.1", and "express": "^4.17.1" In case that sticks out to anyone.

@rtrembecky
Copy link

rtrembecky commented Dec 1, 2021

@zaclittleberry Hi, I have "@slack/bolt": "^3.8.1", and "express": "^4.15.3",, though I'm sorry, I actually lied in my previous post - I'm not using typescript fully in this project, just some soft IDE JS checks. However, I checked the use type and the second argument should always be of the RequestHandler type, so I wonder why it tries to match Application in your case 🤔

@zaclittleberry
Copy link

@rtrembecky thanks for the reply! I'm not sure, either. It doesn't have a type error if I use boltReceiver.app as the second param instead.

In case it is helpful to anyone else: I had to resolve a separate issue after that though, where placing the app.use('/slack/events', boltReceiver.app); anywhere after app.use(express.json()); it wouldn't work. I was getting a vague error about the data not being in the expected format and it returning early. The solution was to just apply express.json() to the routes it was needed for, ex: app.post('/foo/bar', express.json(), async (req, res, next) => { ... });. It may also work to just use slack before your express.json() use, but that wasn't an option in my case.

@github-actions
Copy link

👋 It looks like this issue has been open for 30 days with no activity. We'll mark this as stale for now, and wait 10 days for an update or for further comment before closing this issue out. If you think this issue needs to be prioritized, please comment to get the thread going again! Maintainers also review issues marked as stale on a regular basis and comment or adjust status if the issue needs to be reprioritized.

@github-actions
Copy link

As this issue has been inactive for more than one month, we will be closing it. Thank you to all the participants! If you would like to raise a related issue, please create a new issue which includes your specific details and references this issue number.

@Tstepro
Copy link

Tstepro commented Mar 31, 2022

I wanted to show a pattern that worked for me in nestjs while I was working on solving this. Hopefully this helps.

I define a slack service:

export class SlackService {
  private boltApp: App;
  private readonly receiver: ExpressReceiver;

  constructor(private appService: AppService) {
    this.receiver = new ExpressReceiver({
      signingSecret: process.env.SLACK_SIGNING_SECRET,
      endpoints: '/',
    });

    this.boltApp = new App({
      token: process.env.SLACK_BOT_TOKEN,
      receiver: this.receiver
    });

    this.boltApp.event("app_mention", this.onAppMention.bind(this));
  }
 
   public async onAppMention({ event, client, logger }) {
      try {
        console.log(this);
        console.log(event);
        this.appService.doSomething();
      } catch (error) {
        logger.error(error);
      }
  }
  public use(): Application {
    return this.receiver.app;
  }

}

And then in my main.js where I'm initializing the app module, I reference it like this:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const port = process.env.PORT || 3333;

  const slack = app.get(SlackService);
  app.use('/slack/events', slack.use());

  await app.listen(port);
  Logger.log(
    `🚀 Application is running on: http://localhost:${port}/`
  );
}

@rdohms
Copy link

rdohms commented Apr 6, 2022

has anyone found another solution to the problem stated by @zaclittleberry? Specifically, the broken stream/body reading if express.json() is used before the bolt receiver?

Update: replacing router.use(bodyParser.json()); with router.use(express.json()); did the trick

Update 2: nope, it did not.

Update 3:
Found a workaround solution, to force Express to skip the Json middleware for the slack routes.

router.use(/\/((?!slack).)*/, express.json());

@rdohms
Copy link

rdohms commented Apr 10, 2022

Updating the above.
I seems to work fine for Events, but now that I plugged in a "action" listener, call triggered by actions result in the stream not readable errors.

What I have found but cannot fully understand yet is the different between the readableStates:

// Valid Request
  flowing: null,	
  ended: false,	
  endEmitted: false,	
  sync: true,	
  readingMore: true,	
  dataEmitted: false,

//Invalid Request
  flowing: true,
  ended: true,
  endEmitted: true,
  sync: false,
  readingMore: false,
  dataEmitted: true,

Update 1:
Looks like requests from Action stuff are form-urlencodedand notjson`. Digging from there.

Update 2:
Its not just the json parser, its also the urlencoder, so this now solves my issue:

  router.use(/\/((?!slack).)*/, express.json());
  router.use(/\/((?!slack).)*/, bodyParser.urlencoded({ extended: true }));

I hope this helps more people struggling with this.

@siawyoung
Copy link

I wanted to show a pattern that worked for me in nestjs while I was working on solving this. Hopefully this helps.

I define a slack service:

export class SlackService {
  private boltApp: App;
  private readonly receiver: ExpressReceiver;

  constructor(private appService: AppService) {
    this.receiver = new ExpressReceiver({
      signingSecret: process.env.SLACK_SIGNING_SECRET,
      endpoints: '/',
    });

    this.boltApp = new App({
      token: process.env.SLACK_BOT_TOKEN,
      receiver: this.receiver
    });

    this.boltApp.event("app_mention", this.onAppMention.bind(this));
  }
 
   public async onAppMention({ event, client, logger }) {
      try {
        console.log(this);
        console.log(event);
        this.appService.doSomething();
      } catch (error) {
        logger.error(error);
      }
  }
  public use(): Application {
    return this.receiver.app;
  }

}

And then in my main.js where I'm initializing the app module, I reference it like this:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const port = process.env.PORT || 3333;

  const slack = app.get(SlackService);
  app.use('/slack/events', slack.use());

  await app.listen(port);
  Logger.log(
    `🚀 Application is running on: http://localhost:${port}/`
  );
}

Thanks so much for this, it was a lifesaver!

By the way, if anyone else is using this as well and is wondering why /slack/install isn't working, it's because it's being nested as /slack/events/slack/install, which isn't great. To fix this, I modified the above to use app.use('/', slack.use()) and removed endpoints: '/' from the express receiver configuration.

@n6rayan
Copy link

n6rayan commented Sep 6, 2023

I wanted to show a pattern that worked for me in nestjs while I was working on solving this. Hopefully this helps.
I define a slack service:

export class SlackService {
  private boltApp: App;
  private readonly receiver: ExpressReceiver;

  constructor(private appService: AppService) {
    this.receiver = new ExpressReceiver({
      signingSecret: process.env.SLACK_SIGNING_SECRET,
      endpoints: '/',
    });

    this.boltApp = new App({
      token: process.env.SLACK_BOT_TOKEN,
      receiver: this.receiver
    });

    this.boltApp.event("app_mention", this.onAppMention.bind(this));
  }
 
   public async onAppMention({ event, client, logger }) {
      try {
        console.log(this);
        console.log(event);
        this.appService.doSomething();
      } catch (error) {
        logger.error(error);
      }
  }
  public use(): Application {
    return this.receiver.app;
  }

}

And then in my main.js where I'm initializing the app module, I reference it like this:

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  const port = process.env.PORT || 3333;

  const slack = app.get(SlackService);
  app.use('/slack/events', slack.use());

  await app.listen(port);
  Logger.log(
    `🚀 Application is running on: http://localhost:${port}/`
  );
}

Thanks so much for this, it was a lifesaver!

By the way, if anyone else is using this as well and is wondering why /slack/install isn't working, it's because it's being nested as /slack/events/slack/install, which isn't great. To fix this, I modified the above to use app.use('/', slack.use()) and removed endpoints: '/' from the express receiver configuration.

Did anyone manage to implement an installation store with this solution? It seems when you do, it doesn't inject the token into requests.

@oletrn
Copy link

oletrn commented Jan 15, 2024

Did anyone manage to implement an installation store with this solution? It seems when you do, it doesn't inject the token into requests.

@n6rayan Any updates since you had this issue? I'm facing exactly that with my NestJS and Bolt-js set-up via app.use('/', slack.use()). Have failed to figure it out so far. @siawyoung Do you remember if you had a similar issue in your setup?

UPD: have managed to sort it out. It's essential to maintain the same data structure in production installationStore that is used in FileInstallationStore. Also, it's important to clean up older installations from DB, as some can be duplicates with invalid token.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
auto-triage-stale discussion M-T: An issue where more input is needed to reach a decision
Projects
None yet
Development

No branches or pull requests