メッセージ・イベントのリスニング

アプリが受信可能なメッセージをリッスンするには、message 型でないイベントを除外する message() メソッドを使用します。

message() は、string 型か RegExp 型の、指定パターンに一致しないメッセージを除外する pattern パラメーター(指定は必須ではありません)を受け付けます。

// 特定の文字列、この場合 👋絵文字を含むメッセージと一致
app.message(':wave:', async ({ message, say }) => {
  await say(`Hello, <@${message.user}>`);
});

正規表現(RegExp) パターンの使用

文字列の代わりに 正規表現(RegExp) パターンを使用すると、より細やかなマッチングが可能です。

RegExp の一致結果はすべて context.matches に保持されます。

app.message(/^(hi|hello|hey).*/, async ({ context, say }) => {
  // context.matches の内容が特定の正規表現と一致
  const greeting = context.matches[0];

  await say(`${greeting}, how are you?`);
});

メッセージの送信

リスナー関数内では、その実行に関連付けられた会話 (例:リスナー実行のトリガーが発生したイベント・アクションが発生したチャンネル) があるとき say() を使用できます。 say() は、シンプルなメッセージを送信するための文字列か、もっと複雑なメッセージを送信するための JSON ペイロードを受け付けます。渡されたメッセージのペイロードは、関連付けられた会話へ送信されます。

リスナー関数以外の場所でメッセージを送信したい場合や、より高度な操作 (特定のエラーの処理など) を実行したい場合は、Bolt インスタンスにアタッチされた client を使用して chat.postMessage を呼び出します。

// "knock knock" を含むメッセージをリッスンし、 "who's there?" というメッセージをイタリック体で送信
app.message('knock knock', async ({ message, say }) => {
  await say(`_Who's there?_`);
});

ブロックを用いたメッセージの送信

say() は、より複雑なメッセージペイロードを受け付けるので、メッセージに機能やリッチな構造を与えることが容易です。

リッチなメッセージレイアウトをアプリに追加する方法については、API サイトのガイドを参照し、Block Kit ビルダーの一般的なアプリフローのテンプレートを確認してください。

// 誰かが 📅 絵文字でリアクションした時に、日付ピッカー block を送信
app.event('reaction_added', async ({ event, say }) => {
  if (event.reaction === 'calendar') {
    await say({
      blocks: [{
        "type": "section",
        "text": {
          "type": "mrkdwn",
          "text": "Pick a date for me to remind you"
        },
        "accessory": {
          "type": "datepicker",
          "action_id": "datepicker_remind",
          "initial_date": "2019-04-28",
          "placeholder": {
            "type": "plain_text",
            "text": "Select a date"
          }
        }
      }]
    });
  }
});

イベントのリスニング

Events API イベントのリスニングは、Slack アプリの設定画面でサブスクリプション設定を行った上で event() メソッドを使用します。これにより、Slack で何かが発生した (例:ユーザーがメッセージにリアクションした、チャンネルに参加した) ときに Bolt アプリ側で処理を実行できます。

event() メソッドは、文字列型の eventType を指定する必要があります。

const welcomeChannelId = 'C12345';

// 新しいユーザーがワークスペースに加入したタイミングで、指定のチャンネルにメッセージを送信して自己紹介を促す
app.event('team_join', async ({ event, client }) => {
  try {
    const result = await client.chat.postMessage({
      channel: welcomeChannelId,
      text: `Welcome to the team, <@${event.user}>! 🎉 You can introduce yourself in this channel.`
    });
    console.log(result);
  }
  catch (error) {
    console.error(error);
  }
});

メッセージのサブタイプのフィルタリング

message() リスナーは event('message') と等価の機能を提供します。

イベントのサブタイプをフィルタリングしたい場合、組み込みの matchEventSubtype() ミドルウェアを使用できます。 bot_messagemessage_replied のような一般的なメッセージサブタイプの情報は、メッセージイベントのドキュメントを参照してください。

// bot からのメッセージ全てと一致
app.message(subtype('bot_message'), ({ message }) => {
  console.log(`The bot user ${message.user} said ${message.text}`);
});

Web API の使用

Web API メソッドを呼び出すには、リスナー関数の引数に client として提供されている WebClient を使用します。このインスタンスが使用するトークンは、Bolt アプリの初期化時に指定されたもの もしくは Slack からのリクエストに対して authorize 関数から返されたものが設定されます。組み込みの OAuth サポートは、この後者のケースをデフォルトでハンドリングします。

Bolt アプリケーションは、トップレベルに app.client も持っています。このインスタンスには、トークンをメソッド呼び出しのパラメーターとして都度指定します。Slack からのリクエストが authorize されないユースケースや、リスナー関数の外で Web API を呼び出したい場合は、このトップレベルの app.client を使用します。

トップレベルのクライアントを使ってもリスナー関数でのクライアントを使っても、WebClient が提供するメソッドを呼び出すと、それへの Slack からのレスポンスを含む Promise の値が返されます。

OrG 全体へのインストール機能の導入により、いくつかの Web API は、動作しているワークスペースを伝えるために team_id パラメーターを必要とします。Bolt for JavaScript は、この team_id を Slack から受け取ったペイロードを元に判定し、client インスタンスに設定します。これは、既存のアプリケーションにとっても OrG 全体へのインストールに対応する上で有用です。既存の Web API 呼び出しの処理をアップデートする必要はありません。

// September 30, 2019 11:59:59 PM を Unix エポックタイムで表示
const whenSeptemberEnds = 1569887999;

app.message('wake me up', async ({ message, context }) => {
  try {
    // トークンを用いて chat.scheduleMessage 関数を呼び出す
    const result = await app.client.chat.scheduleMessage({
      // アプリの初期化に用いたトークンを `context` オブジェクトに保存
      token: context.botToken,
      channel: message.channel,
      post_at: whenSeptemberEnds,
      text: 'Summer has come and passed'
    });
  }
  catch (error) {
    console.error(error);
  }
});

アクションのリスニング

Bolt アプリは action メソッドを用いて、ボタンのクリック、メニューの選択、メッセージショートカットなどのユーザーのアクションをリッスンすることができます。

アクションは文字列型の action_id または RegExp オブジェクトでフィルタリングできます。 action_id は、Slack プラットフォーム上のインタラクティブコンポーネントの一意の識別子として機能します。

すべての action() の例で ack() が使用されていることに注目してください。Slack からイベントを受信したことを確認するために、アクションリスナー内で ack() 関数を呼び出す必要があります。これについては、「イベントの確認」 セクションで説明しています。

注: Bolt 2.x からメッセージショートカット(以前はメッセージアクションと呼ばれていました)は action() ではなく shortcut() メソッドを使用するようになりました。この変更については 2.x マイグレーションガイドを参照してください。

// action_id が "approve_button" のインタラクティブコンポーネントがトリガーされる毎にミドルウェアが呼び出される
app.action('approve_button', async ({ ack, say }) => {
  await ack();
  // アクションを反映してメッセージをアップデート
});

制約付きオブジェクトを使用したアクションのリスニング

制約付きのオブジェクトを使って、 callback_idblock_id 、および action_id (またはそれらの組み合わせ) をリッスンすることができます。オブジェクト内の制約には、文字列型または RegExp オブジェクトを使用できます。

// action_id が 'select_user' と一致し、block_id が 'assign_ticket' と一致する場合のみミドルウェアが呼び出される
app.action({ action_id: 'select_user', block_id: 'assign_ticket' },
  async ({ body, action, ack, context }) => {
    await ack();
    try {
      const result = await app.client.reactions.add({
        token: context.botToken,
        name: 'white_check_mark',
        timestamp: action.ts,
        channel: body.channel.id
      });
    }
    catch (error) {
      console.error(error);
    }
  });

アクションへの応答

アクションへの応答には、主に 2 つのやり方があります。1 つ目の (最も一般的な) やり方は say 関数の利用です。 say 関数は、Slack 内のイベントが発生した会話(チャンネルや DM)へメッセージを返します。

アクションに応答する 2 つ目の方法は respond() です。これはアクションに紐付けられている response_url を用いたメッセージの送信をシンプルに行うためのユーティリティです。

// action_id が "approve_button" のインタラクティブコンポーネントがトリガーされる毎にミドルウェアが呼び出される
app.action('approve_button', async ({ ack, say }) => {
  // アクションリクエストの確認
  await ack();
  await say('Request approved 👍');
});

respond() の使用

respond()response_url を呼び出すためのユーティリティであるため、それを直接使うときと同様に動作します。新しいメッセージのペイロードと、オプショナルな引数である response_type (値は in_channel または ephemeral )、 replace_originaldelete_original を含む JSON オブジェクトを渡すことができます。

// "user_select" の action_id がトリガーされたアクションをリッスン
app.action('user_choice', async ({ action, ack, respond }) => {
  await ack();
  await respond(`You selected <@${action.selected_user}>`);
});

イベントの確認

アクション(action)、コマンド(command)、およびオプション(options)イベントは、必ず ack() 関数を用いて確認する必要があります。これにより Slack 側にイベントが正常に受信されたことを知らせることができ、それに応じて Slack のユーザーインターフェイスが更新されます。イベントのタイプによっては、確認の通知方法が異なる場合があります。たとえば、ダイアログの送信を確認するとき、送信内容にエラーがあればバリデーションエラーとともに ack() を呼び出しますが、送信内容が問題なければ、そのようなパラメータなしで ack() を呼び出します。

この ack() による応答は 3 秒以内に行う必要があります。新しいメッセージの送信や、データベースからの情報の取得などを行う前に、リクエストを受けてすぐに ack() を呼び出して応答を返してしまうことをおすすめします。

// Regex でメールアドレスが有効かチェック
let isEmail = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/
// 制約付きのオブジェクト を使用して ticket_submit という callback_id を持つダイアログ送信をリッスン
app.action({ callback_id: 'ticket_submit' }, async ({ action, ack }) => {
  // メールアドレスが有効。ダイアログを受信
  if (isEmail.test(action.submission.email)) {
    await ack();
  } else {
    // メールアドレスが無効。エラーを確認
    await ack({
      errors: [{
        "name": "email_address",
        "error": "Sorry, this isn’t a valid email"
      }]
    });
  }
});

ショートカットのリスニング

shortcut() メソッドは、グローバルショートカットメッセージショートカットの両方をサポートします。

ショートカットは、テキスト入力エリアや検索バーから起動できる Slack クライアント内の UI エレメントです。グローバルショートカットは、コンポーザーメニューまたは検索メニューから呼び出すことができます。メッセージショートカットは、メッセージのコンテキストメニュー内にあります。shortcut() メソッドを使って、これらのショートカットのイベントをリッスンすることができます。このメソッドには callback_id を文字列または正規表現のデータ型で設定します。

グローバルショートカットのイベントは Slack へイベントを受信したことを知らせるために ack() メソッドで確認する必要があります。

グローバルショートカットのペイロードは、ユーザーの実行アクションの確認のためにモーダルを開くなどの用途に使用できる trigger_id を含んでいます。

⚠️ グローバルショートカットのペイロードは チャンネル ID は含んでいない ことに注意してください。もしあなたのアプリがチャンネル ID を知る必要があれば、モーダル内で conversations_select エレメントを使用できます。 メッセージショートカットのペイロードはチャンネル ID を含みます。

// open_modal というグローバルショートカットはシンプルなモーダルを開く
app.shortcut('open_modal', async ({ shortcut, ack, context }) => {
  // グローバルショートカットリクエストの確認
  ack();

  try {
    // 組み込みの WebClient を使って views.open API メソッドを呼び出す
    const result = await app.client.views.open({
      // `context` オブジェクトに保持されたトークンを使用
      token: context.botToken,
      trigger_id: shortcut.trigger_id,
      view: {
        "type": "modal",
        "title": {
          "type": "plain_text",
          "text": "My App"
        },
        "close": {
          "type": "plain_text",
          "text": "Close"
        },
        "blocks": [
          {
            "type": "section",
            "text": {
              "type": "mrkdwn",
              "text": "About the simplest modal you could conceive of :smile:\n\nMaybe <https://api.slack.com/reference/block-kit/interactive-components|*make the modal interactive*> or <https://api.slack.com/surfaces/modals/using#modifying|*learn more advanced modal use cases*>."
            }
          },
          {
            "type": "context",
            "elements": [
              {
                "type": "mrkdwn",
                "text": "Psssst this modal was designed using <https://api.slack.com/tools/block-kit-builder|*Block Kit Builder*>"
              }
            ]
          }
        ]
      }
    });

    console.log(result);
  }
  catch (error) {
    console.error(error);
  }
});

制約付きオブジェクトを使用したショートカットのリスニング

制約付きオブジェクトを使って callback_idtype によるリスニングができます。オブジェクト内の制約は文字列型または RegExp オブジェクトを使用できます。

  // callback_id が 'open_modal' と一致し type が 'message_action' と一致する場合のみミドルウェアが呼び出される
  app.shortcut({ callback_id: 'open_modal', type: 'message_action' }, async ({ shortcut, ack, context, client }) => {
    try {
      // ショートカットリクエストの確認
      await ack();

      // 組み込みの WebClient を使って views.open API メソッドを呼び出す
      const result = await app.client.views.open({
        // `context` オブジェクトに保持されたトークンを使用
        token: context.botToken,
        trigger_id: shortcut.trigger_id,
        view: {
          type: "modal",
          title: {
            type: "plain_text",
            text: "My App"
          },
          close: {
            type: "plain_text",
            text: "Close"
          },
          blocks: [
            {
              type: "section",
              text: {
                type: "mrkdwn",
                text: "About the simplest modal you could conceive of :smile:\n\nMaybe <https://api.slack.com/reference/block-kit/interactive-components|*make the modal interactive*> or <https://api.slack.com/surfaces/modals/using#modifying|*learn more advanced modal use cases*>."
              }
            },
            {
              type: "context",
              elements: [
                {
                  type: "mrkdwn",
                  text: "Psssst this modal was designed using <https://api.slack.com/tools/block-kit-builder|*Block Kit Builder*>"
                }
              ]
            }
          ]
        }
      });

      console.log(result);
    }
    catch (error) {
      console.error(error);
    }
  });

コマンドのリスニングと応答

スラッシュコマンドが実行されたイベントをリッスンするには、アプリで command() メソッドを使用します。メソッドの使用には文字列型の commandName の指定が必要です。

アプリがスラッシュコマンドのイベントを受け取ったことを ack() の実行によって Slack に通知する必要があります。

スラッシュコマンドへの応答には 2 つのやり方があります。1 つ目の方法は、文字列または JSON ペイロードを受け取る say() で、2 つ目は response_url を簡単に利用するためのユーティリティである respond() です。これらについては、「アクションへの応答」セクションで詳しく説明しています。

Slack アプリの管理画面でスラッシュコマンドを設定するとき、そのスラッシュコマンドの Request URL に(https://{ドメイン} に続いて) /slack/events を指定するようにしてください。

// この echo コマンドは ただ、その引数を(やまびこのように)おうむ返しする
app.command('/echo', async ({ command, ack, say }) => {
  // コマンドリクエストを確認
  await ack();

  await say(`${command.text}`);
});

モーダルの開始

モーダルは、ユーザー情報を収集したり、動的な表示を実現するためのインターフェースです。モーダルは、有効な trigger_idビュー部分のペイロード を組み込みの API クライアントによる views.open メソッドの呼び出しに渡すことで開始することができます。

trigger_id はスラッシュコマンド、ボタンの押下、メニューの選択などによって Request URL に送信されたペイロードの項目として入手することができます。

モーダルの生成についてのより詳細な情報は API ドキュメントを参照してください。

// コマンド起動をリッスン
app.command('/ticket', async ({ ack, body, client }) => {
  // コマンドのリクエストを確認
  await ack();

  try {
    const result = await client.views.open({
      // 適切な trigger_id を受け取ってから 3 秒以内に渡す
      trigger_id: body.trigger_id,
      // view の値をペイロードに含む
      view: {
        type: 'modal',
        // callback_id が view を特定するための識別子
        callback_id: 'view_1',
        title: {
          type: 'plain_text',
          text: 'Modal title'
        },
        blocks: [
          {
            type: 'section',
            text: {
              type: 'mrkdwn',
              text: 'Welcome to a modal with _blocks_'
            },
            accessory: {
              type: 'button',
              text: {
                type: 'plain_text',
                text: 'Click me!'
              },
              action_id: 'button_abc'
            }
          },
          {
            type: 'input',
            block_id: 'input_c',
            label: {
              type: 'plain_text',
              text: 'What are your hopes and dreams?'
            },
            element: {
              type: 'plain_text_input',
              action_id: 'dreamy_input',
              multiline: true
            }
          }
        ],
        submit: {
          type: 'plain_text',
          text: 'Submit'
        }
      }
    });
    console.log(result);
  }
  catch (error) {
    console.error(error);
  }
});

モーダルの更新と多重表示

モーダルでは、複数のモーダルをスタックのように積み重ねて表示できます。views.open という API を呼び出すと、まず親の(最初の)モーダルが表示されます。この最初の呼び出しの後、views.update を実行することでそのビューを書き換えることもできますし、最初に述べたように views.push で新しいモーダルを積み重ねて表示することもできます。

views.update
モーダルの更新には、組み込みの API クライアントを使って views.update を呼び出します。この API 呼び出しには、そのモーダルを開いたときに生成された view_id と、更新後の内容を表現する blocks の配列を含む新しい view を渡します。ユーザーが既存のモーダル内の要素とインタラクションを行なった(例:ボタンを押す、メニューから選択する)ことをトリガーにビューを更新する場合、そのリクエストの bodyview_id が含まれます。

views.push
モーダルのスタックに新しいモーダルを積み重ねるためには、組み込みの API クライアントを用いて views.push を呼び出します。この API 呼び出しには、有効な trigger_id と、新しく生成する ビュー部分のペイロードを渡します。views.push の引数は モーダルを開始するときと同様です。最初のモーダルを開いた後、その上にさらに二つまで追加のモーダルをスタックに積み重ねることができます。

より詳細な情報は API ドキュメントを参照してください。

// action_id: button_abc のボタンを押すイベントをリッスン
// (そのボタンはモーダルの中にあるという想定)
app.action('button_abc', async ({ ack, body, client }) => {
  // ボタンを押したイベントを確認
  await ack();

  try {
    const result = await client.views.update({
      // リクエストに含まれる view_id を渡す
      view_id: body.view.id,
      // 競合状態を防ぐために更新前の view に含まれる hash を指定
      hash: body.view.hash,
      // 更新された view の値をペイロードに含む
      view: {
        type: 'modal',
        // callback_id が view を特定するための識別子
        callback_id: 'view_1',
        title: {
          type: 'plain_text',
          text: 'Updated modal'
        },
        blocks: [
          {
            type: 'section',
            text: {
              type: 'plain_text',
              text: 'You updated the modal!'
            }
          },
          {
            type: 'image',
            image_url: 'https://media.giphy.com/media/SVZGEcYt7brkFUyU90/giphy.gif',
            alt_text: 'Yay! The modal was updated'
          }
        ]
      }
    });
    console.log(result);
  }
  catch (error) {
    console.error(error);
  }
});

モーダルでの送信のリスニング

モーダルのペイロードが入力のブロックを含む場合、その入力値を受け取るために view_submission のイベントをリッスンする必要があります。view_submission イベントをリッスンするためには、組み込みの view() メソッドを使用します。

view() メソッドは、文字列型または RegeExp型 の callback_id を必要とします。

input ブロックの値は state オブジェクトを参照することで取得できます。state 内には values というオブジェクトがあり、これは block_id と一意な action_id に紐づける形で入力値を保持しています。

より詳細な情報は API ドキュメントを参照してください。

// モーダルでのデータ送信イベントを処理します
app.view('view_b', async ({ ack, body, view, client }) => {
  // モーダルでのデータ送信イベントを確認
  await ack();

  // 入力値を使ってやりたいことをここで実装 - ここでは DB に保存して送信内容の確認を送っている

  // block_id: block_1 という input ブロック内で action_id: input_a の場合の入力
  const val = view['state']['values']['block_1']['input_a'];
  const user = body['user']['id'];

  // ユーザーに対して送信するメッセージ
  let msg = '';
  // DB に保存
  const results = await db.set(user.input, val);

  if (results) {
    // DB への保存が成功
    msg = 'Your submission was successful';
  } else {
    msg = 'There was an error with your submission';
  }

  // ユーザーにメッセージを送信
  try {
    await client.chat.postMessage({
      channel: user,
      text: msg
    });
  }
  catch (error) {
    console.error(error);
  }

});

オプションのリスニングと応答

options() メソッドは、Slack からのオプション(セレクトメニュー内の動的な選択肢)をリクエストするペイロードをリッスンします。 action() と同様に、文字列型の action_id または制約付きオブジェクトが必要です。

external_select メニューには action_id を使用することをおすすめしますが、ダイアログはまだ Block Kit をサポートしていないため、制約オブジェクトを用いて callback_id でフィルタリングする必要があります。

オプションのリクエストへの応答には、適切なオプションを指定して ack() を実行する必要があります。API サイトに掲載されているexternal_select の応答の例ダイアログ応答の例を参考にしてください。

// external_select オプションリクエストに応答する例
app.options('external_action', async ({ options, ack }) => {
  // チームまたはチャンネル情報を取得
  const results = await db.get(options.team.id);

  if (results) {
    let options = [];
    // ack 応答 するために options 配列に情報をプッシュ
    for (const result in results) {
      options.push({
        "text": {
          "type": "plain_text",
          "text": result.label
        },
        "value": result.value
      });
    }

    await ack({
      "options": options
    });
  } else {
    await ack();
  }
});

OAuth フローの実装

複数のワークスペースにインストールされる Slack アプリは OAuth フローを実装し、アクセストークンなどのインストール時に取得した情報をセキュアな方法で保存しておく必要があります。Bolt for JavaScript では、clientIdclientSecretstateSecretscopesApp 初期化時に指定すると OAuth フローのためのルートのセットアップや state パラメーターを検証する機能が有効になります。組み込みの OAuth サポートモジュールは ExpressReceiver を使用している場合のみ利用可能です。もしカスタムのレシーバーを実装して利用している場合は、私たちが提供している OAuth ライブラリを利用していください。 Bolt for JavaScript の組み込みのモジュールもこれを内部的に利用しています。

Bolt for JavaScript は、アプリのインストールフローを完了した後の遷移先の URL である Redirect URL のためのパスとして slack/oauth_redirect を有効にします。この URL を Slack アプリの設定画面の OAuth and Permissions セクション内で Redirect URL として指定してください。この設定のカスタマイズは以下で説明する installerOptions を指定することで可能です。

Bolt for JavaScript は slack/install というパスも生成します。これは、Slack アプリのダイレクトインストールのために Add to Slack ボタンを置く場合に指定できる URL です。アプリはすでにインストールされていて、さらにユーザーから追加の認可情報(例:ユーザートークンの発行)な場合や、何らかの理由で動的にインストール用の URL を生成したい場合は、ExpressReceiver を自前でインスタンス化し、それを receiver という変数に代入した上で receiver.installer.generateInstallUrl() を呼び出してください。詳細は OAuth ライブラリのドキュメントgenerateInstallUrl() を参照してください。

Slack の OAuth インストールフローについてもっと知りたい場合は API ドキュメントを参照してください。

Enterprise Grid の OrG 全体へのインストールへの対応を追加する場合、Bolt for JavaScript のバージョン 3.0.0 以上を利用してください。また Slack アプリの設定画面で Org Level Apps の設定が有効になっていることを確認してください。

const app = new App({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  clientId: process.env.SLACK_CLIENT_ID,
  clientSecret: process.env.SLACK_CLIENT_SECRET,
  stateSecret: 'my-state-secret',
  scopes: ['channels:read', 'groups:read', 'channels:manage', 'chat:write', 'incoming-webhook'],
  installationStore: {
    storeInstallation: async (installation) => {
      // 実際のデータベースに保存するために、ここのコードを変更
      if (installation.isEnterpriseInstall) {
        // OrG 全体へのインストールに対応する場合
        return await database.set(installation.enterprise.id, installation);
      } else {
        // 単独のワークスペースへのインストールの場合
        return await database.set(installation.team.id, installation);
      }
      throw new Error('Failed saving installation data to installationStore');
    },
    fetchInstallation: async (installQuery) => {
      // 実際のデータベースから取得するために、ここのコードを変更
      if (installQuery.isEnterpriseInstall && installQuery.enterpriseId !== undefined) {
        // OrG 全体へのインストール情報の参照
        return await database.get(installQuery.enterpriseId);
      }
      if (installQuery.teamId !== undefined) {
        // 単独のワークスペースへのインストール情報の参照
        return await database.get(installQuery.teamId);
      }
      throw new Error('Failed fetching installation');
    },
  },
});

OAuth デフォルト設定をカスタマイズ

installerOptions を使って OAuth モジュールのデフォルト設定を上書きすることができます。このカスタマイズされた設定は App の初期化時に渡します。以下の情報を変更可能です:

  • authVersion: 新しい Slack アプリとクラシック Slack アプリの切り替えに使用
  • metadata: セッションに関連する情報の指定に使用
  • installPath: “Add to Slack” ボタンのためのパスを変更するために使用
  • redirectUriPath: Redirect URL を変更するために使用
  • callbackOptions: OAuth フロー完了時の成功・エラー完了画面をカスタマイズするために使用
  • stateStore: 組み込みの ClearStateStore の代わりにカスタムのデータストアを有効にするために使用
const app = new App({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  clientId: process.env.SLACK_CLIENT_ID,
  clientSecret: process.env.SLACK_CLIENT_SECRET,
  scopes: ['channels:read', 'groups:read', 'channels:manage', 'chat:write', 'incoming-webhook'],
  installerOptions: {
      authVersion: 'v1', // デフォルトは 'v2' (クラシック Slack アプリは 'v1')
      metadata: 'some session data',
      installPath: '/slack/installApp',
      redirectUriPath: '/slack/redirect',
      callbackOptions: {
        success: (installation, installOptions, req, res) => {
          // ここで成功時のカスタムロジックを実装
          res.send('successful!');
        }, 
        failure: (error, installOptions , req, res) => {
          // ここでエラー時のカスタムロジックを実装
          res.send('failure');
        }
      },
      stateStore: {
        // `stateStore` を指定する場合は `stateSecret` の設定が不要

        // 第一引数は `generateInstallUrl` メソッドに渡される `InstallUrlOptions` オブジェクト、第二引数は日付オブジェクト
        // state の文字列を応答
        generateStateParam: async (installUrlOptions, date) => {
          // URL の state パラメーターとして使用するランダムな文字列を生成
          const randomState = randomStringGenerator();
          // その値をキャッシュ、データベースに保存
          await myDB.set(randomState, installUrlOptions);
          // データベースに保存されたものを利用可能な値として返却
          return randomState;
        },

        // 第一引数は日付オブジェクトで、第二引数は state を表現する文字列
        // `installUrlOptions` オブジェクトを応答
        verifyStateParam: async (date, state) => {
          // state をキーに、データベースから保存された installOptions を取得
          const installUrlOptions = await myDB.get(randomState);
          return installUrlOptions;
        }
      },
  }
});

ソケットモードの使用

ソケットモード は、アプリに WebSocket での接続と、そのコネクション経由でのデータ受信を可能とします。コネクションをハンドリングするために @slack/bolt@3.0.0 以上では SokcetModeReceiver というレシーバーが提供されています。ソケットモードを使う前に、アプリの管理画面でソケットモードの機能が有効になっているコオを確認しておいてください。

SocketModeReceiver を使う方法は App インスタンスの初期化時にコンストラクターに socketMode: trueappToken: YOUR_APP_TOKEN を渡すだけです。App Level Token は、アプリ管理画面の Basic Information セクションから取得できます。

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

const app = new App({
  token: process.env.BOT_TOKEN,
  socketMode: true,
  appToken: process.env.APP_TOKEN,
});

(async () => {
  await app.start();
  console.log('⚡️ Bolt app started');
})();

ソケットモードレシーバーのカスタム初期化

以下のように @slack/bolt から SocketModeReceiver を import して、カスタムされたインスタンスとして定義することができます。

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

const socketModeReceiver = new SocketModeReceiver({
  appToken: process.env.APP_TOKEN,

  // OAuth フローの実装を合わせて使う場合は、以下を有効にしてください
  // clientId: process.env.CLIENT_ID,
  // clientSecret: process.env.CLIENT_SECRET,
  // stateSecret: 'my-state-secret',
  // scopes: ['channels:read', 'chat:write', 'app_mentions:read', 'channels:manage', 'commands'],
});

const app = new App({
  receiver: socketModeReceiver,
  // OAuth を使うなら以下の token 指定は不要です
  token: process.env.BOT_TOKEN
});

(async () => {
  await app.start();
  console.log('⚡️ Bolt app started');
})();

ワークフローステップの概要

(アプリによる)ワークフローステップ(Workflow Steps from Apps) は、ワークフロービルダーにおけるワークフローに組み込み可能なカスタムのワークフローステップを任意の Slack アプリが提供することを可能とします。

ワークフローステップは、三つの異なるユーザーイベントから構成されます:

  • ワークフロー作成者がワークフローにカスタムステップを追加・または編集する
  • ワークフロー作成者がステップの設定を保存・更新する
  • ワークフローの利用者がそのワークフローステップを実行する

ワークフローステップを機能させるためには、これら三つのイベント全てを適切にハンドリングする必要があります。

ワークフローステップのさらなる詳細については API ドキュメントを参考にしてください。


ステップの定義

ワークフローステップを作るための手段として Bolt は WorkflowStep というクラスを提供しています。

新しい WorkflowStep インスタンスの生成には、そのステップの callback_id と設定オブジェクトを渡します。

設定オブジェクトには editsaveexecute という三つのプロパティがあります。これらのそれぞれは単一のコールバック関数、またはコールバック関数の配列である必要があります。すべてのコールバック関数は、ワークフローステップのイベントに関する情報を保持しする step オブジェクトにアクセスすることができます。

WorkflowStep インスタンスを生成したら、それを app.step() メソッドに渡します。これによって、Bolt アプリは対象のワークフローステップのイベントをリッスンしたり、設定オブジェクトが提供するコールバック関数を使ってイベントに応答したりすることができるようになります。

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

// いつも通り Bolt アプリを初期化
const app = new App({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  token: process.env.SLACK_BOT_TOKEN,
});

// WorkflowStep インスタンスを生成
const ws = new WorkflowStep('add_task', {
  edit: async ({ ack, step, configure }) => {},
  save: async ({ ack, step, update }) => {},
  execute: async ({ step, complete, fail }) => {},
});

app.step(ws);

ステップの追加・編集

ワークフローの作成者が、アプリが提供するステップをワークフローに追加(またはその設定を変更)するタイミングで、アプリは workflow_step_edit というイベントを受信します。このイベントの受信時に WorkflowStep 設定オブジェクト内の edit コールバック関数が実行されます。

このとき、ワークフロー作成・変更のどちらの場合でも、アプリはワークフローステップ設定のためのモーダルを応答する必要があります。このモーダルは、ワークフローステップに固有の設定である必要があり、通常のモーダルにはない制約があります。最もわかりやすいものとしては、title​submit​close プロパティを設定することができません。また、デフォルトの設定では、この設定モーダルの callback_id はワークフローステップのものと同じものが使用されます。

edit コールバック関数の中では モーダルの view のうち blocks だけを渡すだけで簡単にステップ設定モーダルをオープンすることができる configure() というユーティリティ関数が利用できます。これは、必要な入力内容が揃うまで設定の保存を無効にする submit_disabled というオプションを true に設定します。

設定モーダルを開く処理に関するさらなる詳細は、ドキュメントを参考にしてください。

const ws = new WorkflowStep('add_task', {
  edit: async ({ ack, step, configure }) => {
    await ack();

    const blocks = [
      {
        type: 'input',
        block_id: 'task_name_input',
        element: {
          type: 'plain_text_input',
          action_id: 'name',
          placeholder: {
            type: 'plain_text',
            text: 'Add a task name',
          },
        },
        label: {
          type: 'plain_text',
          text: 'Task name',
        },
      },
      {
        type: 'input',
        block_id: 'task_description_input',
        element: {
          type: 'plain_text_input',
          action_id: 'description',
          placeholder: {
            type: 'plain_text',
            text: 'Add a task description',
          },
        },
        label: {
          type: 'plain_text',
          text: 'Task description',
        },
      },
    ];

    await configure({ blocks });
  },
  save: async ({ ack, step, update }) => {},
  execute: async ({ step, complete, fail }) => {},
});

ステップの設定の保存

ワークフローステップの設定モーダルが開いたら、アプリはワークフロー作成者がモーダルを送信するイベントである view_submission イベントを待ち受けます。このイベントを受信すると WorkflowStep 設定オブジェクト内の save コールバック関数が実行されます。

save コールバック関数の中では、以下の引数を渡してステップの設定を保存するための update() 関数を利用できます。

  • inputs は、ワークフローステップ実行時にアプリが受け取ることを期待するデータの内容を表現するオブジェクトです
  • outputs は、ステップの実行が正常に完了したとき、同一ワークフロー内の後続のステップに提供するデータの内容を表現するオブジェクトの配列です。
  • step_name は、デフォルトのステップ名を上書きするために使用します
  • step_image_url は、デフォルトのステップのイメージ画像を上書きするために使用します

これら引数をどのように構成するかの詳細は、ドキュメントを参考にしてください。

const ws = new WorkflowStep('add_task', {
  edit: async ({ ack, step, configure }) => {},
  save: async ({ ack, step, view, update }) => {
    await ack();

    const { values } = view.state;
    const taskName = values.task_name_input.name;
    const taskDescription = values.task_description_input.description;
                
    const inputs = {
      taskName: { value: taskName.value },
      taskDescription: { value: taskDescription.value }
    };

    const outputs = [
      {
        type: 'text',
        name: 'taskName',
        label: 'Task name',
      },
      {
        type: 'text',
        name: 'taskDescription',
        label: 'Task description',
      }
    ];

    await update({ inputs, outputs });
  },
  execute: async ({ step, complete, fail }) => {},
});

ステップの実行

ワークフローの利用者によって、アプリが提供するカスタムのワークフローステップが実行されるとき、アプリはworkflow_step_execute というイベントを受信します。このイベントの受信時に WorkflowStep 設定オブジェクト内の execute コールバック関数が実行されます。

save コールバック関数で予め規定された inputs の情報を使って、ここでの処理は、サードパーティの API を呼び出したり、データベースに情報を保存したり、そのユーザーのホームタブを更新したり、outputs オブジェクトを構築することで後続のワークフローステップが利用できる情報を設定したりします。

execute コールバック関数内では、ステップの実行が成功であることを Slack 側に伝える complete() 関数、失敗であることを伝える fail() 関数のいずれかを呼び出す必要があります。

const ws = new WorkflowStep('add_task', {
  edit: async ({ ack, step, configure }) => {},
  save: async ({ ack, step, update }) => {},
  execute: async ({ step, complete, fail }) => {
    const { inputs } = step;

    const outputs = {
      taskName: inputs.taskName.value,
      taskDescription: inputs.taskDescription.value,
    };

    // もし全て OK なら
    await complete({ outputs });

    // もし何か問題が起きたら
    // fail({ error: { message: "Just testing step failure!" } });
  },
});

エラーの処理

注: Bolt 2.x からエラーハンドリングが改善されました!この変更については 2.x マイグレーションガイドを参照してください。

リスナーでエラーが発生した場合は try/catch を使って直接ハンドリングすることをおすすめします。しかし、それでもなおすり抜けてしまうエラーのパターンもあるでしょう。デフォルトでは、このようなエラーはコンソールにログ出力されます。ご自身でこれらをハンドリングするには、app.error(fn) メソッドによって、グローバルエラーハンドラーを定義してください。

app.error((error) => {
  // メッセージ再送信もしくはアプリを停止するかの判断をするためにエラーの詳細を出力して確認
  console.error(error);
});

認可(Authorization)

認可(Authorization)は、Slack からのイベントを処理するにあたって、どの Slack クレデンシャル (ボットトークンなど) を使用可能にするかを決定するプロセスです。

1 つだけのワークスペースにインストールされたカスタムアプリであれば App 初期化時に単に token オプションを使用するだけで OK です。一方で、複数のワークスペースにインストールされる、複数のユーザートークンを使用するといったケースのように、アプリが複数のトークンを処理しなければならない場合があります。このようなケースでは token の代わりに authorize オプションを使用する必要があります。

authorize オプションには、イベントソースを入力値として受け取り、許可された認可されたクレデンシャルを含むオブジェクトを Promise の値として返す関数を指定します。このイベントソースの情報には、 teamId (常に存在します)、 userIdconversationIdenterpriseId のような、イベントが誰によって発生させられたか、どこで発生したかに関する情報が含まれます。

許可されたクレデンシャルには、botTokenuserTokenbotId (アプリがボット自体からのメッセージを無視するために必要です)、 botUserId が含まれます。context オブジェクトに、これ以外の他のプロパティを自由に設定することもできます。

botTokenuserToken は、どちらか、またはその両方を必ず設定してください。say() のようなユーティリティを動作させるには、どちらか一方が存在している必要があります。両方指定した場合、say() では botToken が優先されます。

const app = new App({ authorize: authorizeFn, signingSecret: process.env.SLACK_SIGNING_SECRET });

// 注: これはデモの目的のみの例です
// 実際は重要なデータはセキュリティの高いデータベースに保存してください。このアプリは bot トークンのみを使用すると仮定しています。ここで使われるオブジェクトは、複数ワークスペースにアプリをインストールした場合のクレデンシャルを保管するモデルです。

const installations = [
  {
    enterpriseId: 'E1234A12AB',
    teamId: 'T12345',
    botToken: 'xoxb-123abc',
    botId: 'B1251',
    botUserId: 'U12385',
  },
  {
    teamId: 'T77712',
    botToken: 'xoxb-102anc',
    botId: 'B5910',
    botUserId: 'U1239',
  },
];

const authorizeFn = async ({ teamId, enterpriseId }) => {
  // データベースから team(ワークスペース)を取得
  for (const team of installations) {
    // installations 配列から teamId と enterpriseId(Enterprise Grid の OrG の ID)が一致するかチェック
    if ((team.teamId === teamId) && (team.enterpriseId === enterpriseId)) {
      // 一致したワークスペースのクレデンシャルを使用
      return {
        // 代わりに userToken をセットしても OK
        botToken: team.botToken,
        botId: team.botId,
        botUserId: team.botUserId
      };
    }
  }

  throw new Error('No matching authorizations');
}

会話ストア

Bolt は、会話 (conversation) に関連する state を設定および取得する store をサポートしています。conversation store には以下の 2 つのメソッドがあります。

  • set() は会話の state を変更します。set() は、文字列型の conversationId、任意の型の value、およびオプションの数値型の expiresAt を必要とします。set()Promise を返します。
  • get() は store から会話の state を取得します。get() は文字列型の conversationId を必要とし、その会話の state とともに Promise を返します。

conversationContext() は、他のミドルウェアによる会話の更新を可能にする組み込みのグローバルミドルウェアです。イベントを受け取ると、ミドルウェア関数は context.updateConversation() を使用して状態を設定でき、context.conversation を使用してその state を取得できます。

組み込みの conversation store は、シンプルに会話の state をメモリーに格納します。状況によってはこれで十分ですが、アプリのインスタンスが複数実行されている場合、状態はプロセス間で共有されないため、データベースを使用して会話の state を取得する conversation store を実装することをおすすめします。

const app = new App({
  token,
  signingSecret,
  // クラスを作成する感じで
  convoStore: new simpleConvoStore()
});

// Firebaseのようなデータベースを使い conversation store を実装
class simpleConvoStore {
  set(conversationId, value, expiresAt) {
    // Promise を返す
    return db().ref('conversations/' + conversationId).set({ value, expiresAt });
  }

  get(conversationId) {
    // Promise を返す
    return new Promise((resolve, reject) => {
      db().ref('conversations/' + conversationId).once('value').then((result) => {
        if (result !== undefined) {
          if (result.expiresAt !== undefined && Date.now() > result.expiresAt) {
            db().ref('conversations/' + conversationId).delete();

            reject(new Error('Conversation expired'));
          }
          resolve(result.value)
        } else {
          // Conversation が存在しないエラー
          reject(new Error('Conversation not found'));
        }
      });
    });
  }
}

グローバルミドルウェア

グローバルミドルウェアは、すべての受信イベントに対して、リスナーミドルウェアより前に実行されます。app.use(fn({payload,...,next})) を使用すると、グローバルミドルウェアをいくつでもアプリに追加できます。

グローバルミドルウェアとリスナーミドルウェアは、いずれも、await next() を呼び出して実行チェーンの制御を次のミドルウェアに渡すか、throw を呼び出して以前に実行したミドルウェアチェーンにエラーを渡す必要があります。

たとえば、アプリが、対応する内部認証サービス (SSO プロバイダ、LDAP など) で識別されたユーザーにのみ応答する必要があるとします。この場合、グローバルミドルウェアを使用して認証サービス内のユーザーレコードを検索し、ユーザーが見つからない場合はエラーとなるように定義するのがよいでしょう。

注: Bolt 2.x からグローバルミドルウェアが async 関数をサポートしました!この変更については 2.x マイグレーションガイドを参照してください。

//  Acme ID情報管理プロバイダ上のユーザからの着信イベントと紐つけた認証ミドルウェア
async function authWithAcme({ payload, client, context, next }) {
  const slackUserId = payload.user;
  const helpChannelId = 'C12345';

  // Slack ユーザ ID を使って Acmeシステム上にあるユーザ情報を検索できる関数があるとと仮定
  try {
    const user = await acme.lookupBySlackId(slackUserId)
    
    // 検索できたらそのユーザ情報でコンテクストを生成
    context.user = user;
  } catch (error) {
      // Acme システム上にユーザが存在しないのでエラーをわたし、イベントプロセスを終了
      if (error.message === 'Not Found') {
        await client.chat.postEphemeral({
          channel: payload.channel,
          user: slackUserId,
          text: `Sorry <@${slackUserId}>, you aren't registered in Acme. Please post in <#${helpChannelId}> for assistance.`
        });
        return;
      }

      // 制御とリスナー関数を(もしあれば)前のミドルウェア渡す、もしくはグローバルエラーハンドラに引き渡し
      throw error;
  }
  
  // 制御とリスナー関数を次のミドルウェアに引き渡し
  await next();
}

リスナーミドルウェア

リスナーミドルウェアは、多くのリスナー関数を対象(つまり、複数のリスナー関数を対象としますが、全てのリスナーに実行するわけではないものです)としたロジックの適用に使用でき、リスナーを追加する組み込みメソッドの引数リスト内で、リスナー関数より先に引数として追加されます。ここでは任意の数のリスナーミドルウェアを追加することができます。

組み込みリスナーミドルウェアはいくつか用意されており、例えば、メッセージのサブタイプをフィルタリングする subtype() や、ボットに直接 @ メンションしないメッセージを除外する directMention() のように使用することができます。

もちろん、よりカスタマイズされた機能を追加するために独自のミドルウェアを実装することもできます。カスタムミドルウェアとして動作する関数の実装は await next() を呼び出して制御を次のミドルウェアに渡すか、throw を呼び出して以前に実行されたミドルウェアチェーンにエラーを投げる必要があります。

例として、リスナーが人(ボットではないユーザー)からのメッセージのみを扱うケースを考えてみましょう。このためには、全てのボットメッセージを除外するリスナーミドルウェアを実装します。

注: Bolt 2.x からミドルウェアが async 関数をサポートしました!この変更については 2.x マイグレーションガイドを参照してください。

// 'bot_message' サブタイプを持つメッセージをフィルタリングするリスナーミドルウェア
async function noBotMessages({ message, next }) {
  if (!message.subtype || message.subtype !== 'bot_message') {
    await next();
  }
}

// ボットではなく人間からのメッセージのみを受信するリスナー
app.message(noBotMessages, async ({ message }) => console.log(
  `(MSG) User: ${message.user}
   Message: ${message.text}`
));

context の追加

context オブジェクトは、受信イベントに付加情報を提供するために使用されるもので、全てのリスナーがこれを使用できます。例えば、3rd party のシステムからユーザー情報を追加したり、ミドルウェアのチェインの中で次のミドルウェアが必要とする一時的な状態を追加したりといった用途に利用できます。

context は、ただのオブジェクトなので、いくらでも属性を追加、編集することができます。

async function addTimezoneContext({ payload, client, context, next }) {
  const user = await client.users.info({
    user: payload.user_id,
    include_locale: true
  });

  // ユーザのタイムゾーン情報を追加
  context.tz_offset = user.tz_offset;
  
  // 制御とリスナー関数を次のミドルウェアに引き渡し
  await next();
}

app.command('request', addTimezoneContext, async ({ command, ack, client, context }) => {
  // コマンドリクエストの確認
  await ack();
  // リクエスト時のローカル時間を取得
  const local_hour = (Date.UTC() + context.tz_offset).getHours();

  // チャンネル ID のリクエスト
  const requestChannel = 'C12345';

  const requestText = `:large_blue_circle: *New request from <@${command.user_id}>*: ${command.text}`;

  // 午前 9 時〜午後 5 時以外のリクエストの場合は明日
  if (local_hour > 17 || local_hour < 9) {
    // ローカル時間の明日午前 9 時までの差分を取得する関数があると仮定
    const local_tomorrow = getLocalTomorrow(context.tz_offset);

    try {
      // メッセージ送信スケジュールを調整
      const result = await client.chat.scheduleMessage({
        channel: requestChannel,
        text: requestText,
        post_at: local_tomorrow
      });
    }
    catch (error) {
      console.error(error);
    }
  } else {
    try {
      // 送信
      const result = client.chat.postMessage({
        channel: requestChannel,
        text: requestText
      });
    }
    catch (error) {
      console.error(error);
    }
  }
});

ログの表示

Bolt はデフォルトの設定では、標準出力のコンソールにログ上を出力します。どれくらいのどれくらいのログが出力されるかは、コンストラクターの引数の logLevel を指定して、カスタマイズできます。使用可能なログレベルは、頻度の高い方から順に、DEBUGINFOWARNERROR です。

// パッケージから LogLevel をインポート
const { App, LogLevel } = require('@slack/bolt');

// オプションとして、コンストラクタで Log level を設定可能
const app = new App({
  token,
  signingSecret,
  logLevel: LogLevel.DEBUG,
});

コンソール以外へのログ出力の送信

ログの送信先をコンソール以外に設定したり、よりロガーを細かくコントロールしたい場合は、カスタムロガーを実装します。カスタムロガーは、以下のメソッド (Logger インターフェイスに定義されているもの) を実装する必要があります。

メソッド パラメーター 戻り値の型
setLevel() level: LogLevel void
getLevel() なし string (値は error, warn, info, debug のいずれか)
setName() name: string void
debug() ...msgs: any[] void
info() ...msgs: any[] void
warn() ...msgs: any[] void
error() ...msgs: any[] void

非常に単純なカスタム logger では、名前やレベルが無視され、すべてのメッセージがファイルに書き込まれることがあります。

const { App } = require('@slack/bolt');
const { createWriteStream } = require('fs');
const logWritable = createWriteStream('/var/my_log_file');

const app = new App({
  token,
  signingSecret,
  // リテラルオブジェクトとして logger を設定(必要なメソッドを持つクラスを指定するイメージで)
  logger: {
    debug(...msgs): { logWritable.write('debug: ' + JSON.stringify(msgs)); },
    info(...msgs): { logWritable.write('info: ' + JSON.stringify(msgs)); },
    warn(...msgs): { logWritable.write('warn: ' + JSON.stringify(msgs)); },
    error(...msgs): { logWritable.write('error: ' + JSON.stringify(msgs)); },
    setLevel(): { },
    setName(): { },
  },
});

レシーバーのカスタマイズ

レシーバーは、Slack からのイベントを受け付けてパースした後、それを Bolt アプリに伝える責務を担っています。Bolt アプリは、context 情報やリスナーへのイベントの引き渡しを行います。レシーバーの実装は Receiver インターフェイスに準拠している必要があります。

メソッド パラメーター 戻り値の型
init() app: App unknown
start() None Promise
stop() None Promise

init() メソッドは Bolt for JavaScript アプリが生成されたときに呼び出されます。このメソッドはレシーバーに App インスタンスへの参照を与えます。レシーバーはこれを保持して、イベント受信時に呼び出します。

  • await app.processEvent(event) は Slack から送信されてくるイベントを受け取るたびに呼び出されます。ハンドリングされなかったエラーが発生した場合はそれを throw します。

カスタムのレシーバーを使用する場合は、それを App のコンストラクターに渡します。ここで紹介しているコード例は、基本的なカスタムレシーバーの実装例です。

レシーバーについてより深く知りたい場合は、組み込み ExpressReceiver のソースコードを参照してください。

import { EventEmitter } from 'events';
import { createServer } from 'http';
import express from 'express';

// EventEmitter は on() 関数を操作
// https://nodejs.org/api/events.html#events_emitter_on_eventname_listener
class simpleReceiver extends EventEmitter {
  constructor(signingSecret, endpoints) {
    super();
    this.app = express();
    this.server = createServer(this.app);

    for (const endpoint of endpoints) {
      this.app.post(endpoint, this.requestHandler.bind(this));
    }
  }
  
  init(app) {
    this.bolt = app;
  }

  start(port) {
    return new Promise((resolve, reject) => {
      try {
        this.server.listen(port, () => {
          resolve(this.server);
        });
      } catch (error) {
        reject(error);
      }
    });
  }

  stop() {
    return new Promise((resolve, reject) => {
      this.server.close((error) => {
        if (error) {
          reject(error);
          return;
        }
        resolve();
      })
    })
  }

  async requestHandler(req, res) {
    let ackCalled = false;
    // 着信リクエストをパースするparseBody 関数があると仮定
    const parsedReq = parseBody(req);
    const event = {
      body: parsedReq.body,
      // レシーバーが確認作業に重要
      ack: (response) => {
        if (ackCalled) {
          return;
        }
        
        if (response instanceof Error) {
          res.status(500).send();
        } else if (!response) {
          res.send('')
        } else {
          res.send(response);
        }
        
        ackCalled = true;
      }
    };
    await this.bolt.processEvent(event);
  }
}

カスタム HTTP ルートの追加

Bolt の組み込みの ExpressReceiver を使っているなら、カスタムの HTTP ルートを追加するのはとても簡単です。v2.1.0 から ExpressReceiver には router というプロパティが追加されています。これは、さらにルートを追加できるように App 内部で保持している Exprss の Router を public にしたものです。

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

// Bolt の Receiver を明に生成
const receiver = new ExpressReceiver({ signingSecret: process.env.SLACK_SIGNING_SECRET });

// App をこのレシーバーを指定して生成
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  receiver
});

// Slack とのやりとりは App のメソッドで定義
app.event('message', async ({ event, client }) => {
  // Do some slack-specific stuff here
  await client.chat.postMessage(...);
});

// それ以外の Web リクエストの処理は receiver.router のメソッドで定義
receiver.router.post('/secret-page', (req, res) => {
  // ここでは Express のリクエストやレスポンスをそのまま扱う
  res.send('yay!');
});

(async () => {
  await app.start(8080);
  console.log('app is running');
})();