slackapi / bolt-js

A framework to build Slack apps using JavaScript
https://tools.slack.dev/bolt-js/
MIT License
2.74k stars 393 forks source link

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

Closed rmrbytes closed 2 years ago

rmrbytes commented 5 years ago

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 commented 5 years ago

Any Progress on This issue?

Startouf commented 5 years ago

I would also be interested as a possible way to solve https://github.com/slackapi/bolt/issues/283

dcsan commented 4 years ago

is this related to expressReceiver ?

regniblod commented 4 years ago

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 commented 4 years ago

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

aoberoi commented 4 years ago

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.

duke79 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 3 years ago

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 commented 3 years ago
receiver

Thanks @tell-y works like a charm 🙏

jyb247 commented 3 years ago

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:

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 commented 3 years ago

@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 commented 3 years ago

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

captDaylight commented 3 years ago

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 commented 3 years ago

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

captDaylight commented 3 years ago

@jyb247 disregard, thanks for the help!

jyb247 commented 3 years ago

@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 commented 3 years ago

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

senguttuvang commented 3 years ago

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 commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

@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 commented 3 years ago

@8YOne that solved it for me thanks!

matthiassb commented 3 years ago

Is there a simple working example for this scenario?

recurrence commented 3 years ago

I suspect this will resolve this issue ? https://github.com/slackapi/bolt-js/issues/868

rtrembecky commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

@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 commented 2 years ago

@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 commented 2 years ago

@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 commented 2 years ago

@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[bot] commented 2 years ago

👋 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[bot] commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 2 years ago

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 commented 1 year ago

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 commented 9 months ago

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.

shikhanshu commented 3 weeks ago

I am very new to all this, and really have no idea what I am doing. But I have been tasked to add a slack app to an existing web app. And I am trying to follow the examples in this thread.

First I get:

`import { App, ExpressReceiver } from '@slack/bolt';
         ^^^
SyntaxError: Named export 'App' not found. The requested module '@slack/bolt' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from '@slack/bolt';
const { App, ExpressReceiver } = pkg;`

Then if I do:

import App from '@slack/bolt';
import ExpressReceiver from '@slack/bolt';

...

const receiver = new ExpressReceiver({
                 ^

TypeError: ExpressReceiver is not a constructor

same error when trying to create App object.

It's like I am missing something very basic, everyone else seems to be able to have it work with code like above :(

Also, I want to enable socketMode so I can have slack send me the events/mentions/commands to this webapp. I am able to get the events going when creating a completely separate slack bolt app (from the tutorials).

Any pointers will be greatly appreciated.

rtrembecky commented 2 weeks ago

@shikhanshu this looks like a project setup issue, not really related to this. You can google for this error message (omitting the package specifics) and you'll find many results. But you should also be able to follow the error message suggestion and do exactly what's written there:

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