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

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

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

1
2
3
4
5
6
7
8
9
10
// 特定の文字列、この場合 👋絵文字を含むメッセージと一致
app.message(':wave:', async ({ message, say }) => {
  // 新しく投稿されたメッセージだけを処理
  if (message.subtype === undefined
    || message.subtype === 'bot_message'
    || message.subtype === 'file_share'
    || message.subtype === 'thread_broadcast') {
    await say(`Hello, <@${message.user}>`);
  }
});

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

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

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

1
2
3
4
5
6
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 を呼び出します。

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 誰かが 📅 絵文字でリアクションした時に、日付ピッカー 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 を指定する必要があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const welcomeChannelId = 'C12345';

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
// パッケージから subtype をインポート
const { App, subtype } = require('@slack/bolt');

// user からのメッセージの編集と一致
app.message(subtype('message_changed'), ({ event, logger }) => {
  // この if 文は TypeScript でコードを書く際に必要
  if (event.subtype === 'message_changed'
    && !event.message.subtype
    && !event.previous_message.subtype) {
    logger.info(`The user ${event.message.user} changed their message from ${event.previous_message.text} to ${event.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 呼び出しの処理をアップデートする必要はありません。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// September 30, 2019 11:59:59 PM を Unix エポックタイムで表示
const whenSeptemberEnds = 1569887999;

app.message('wake me up', async ({ message, context, logger }) => {
  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) {
    logger.error(error);
  }
});

アクションのリスニング

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

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

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

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

block_actions ペイロードの詳細については、こちら をご覧ください。リスナー内からビューの完全なペイロードにアクセスするには、コールバック関数内で body 引数を参照します。

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// action_id が 'select_user' と一致し、block_id が 'assign_ticket' と一致する場合のみミドルウェアが呼び出される
app.action({ action_id: 'select_user', block_id: 'assign_ticket' },
  async ({ body, client, ack, logger }) => {
    await ack();
    try {
      if (body.message) {
        const result = await client.reactions.add({
          name: 'white_check_mark',
          timestamp: body.message.ts,
          channel: body.channel.id
        });

        logger.info(result);
      }
    }
    catch (error) {
      logger.error(error);
    }
  });

アクションへの応答

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

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

1
2
3
4
5
6
// 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 オブジェクトを渡すことができます。

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

リクエストの確認

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Regex でメールアドレスが有効かチェック
let isEmail = /^[\w\-\.]+@([\w\-]+\.)+[\w\-]+$/;
// 制約付きのオブジェクト を使用して ticket_submit という callback_id を持つモーダル送信をリッスン
app.view('ticket_submit', async ({ ack, view }) => {
  // block_id が `email_address` の input ブロックからメールアドレスを取得
  const email = view.state.values['email_address']['input_a'].value;

  // メールアドレスが有効。モーダルを受信
  if (email && isEmail.test(email)) {
    await ack();
  } else {
    // メールアドレスが無効。エラーを確認
    await ack({
      "response_action": "errors",
      errors: {
        "email_address": "Sorry, this isn’t a valid email"
      }
    });
  }
});

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

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

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

⚠️ 同じ対象にマッチする正規表現の shortcut() を複数使用する場合、マッチする 全ての リスナーが実行されることに注意してください。そのような挙動を意図しない場合は、これが発生しないよう正規表現をデザインしてください。

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// open_modal というグローバルショートカットはシンプルなモーダルを開く
app.shortcut('open_modal', async ({ shortcut, ack, context, logger }) => {
  // グローバルショートカットリクエストの確認
  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*>"
              }
            ]
          }
        ]
      }
    });

    logger.info(result);
  }
  catch (error) {
    logger.error(error);
  }
});

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
  // callback_id が 'open_modal' と一致し type が 'message_action' と一致する場合のみミドルウェアが呼び出される
  app.shortcut({ callback_id: 'open_modal', type: 'message_action' }, async ({ shortcut, ack, context, client, logger }) => {
    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*>"
                }
              ]
            }
          ]
        }
      });

      logger.info(result);
    }
    catch (error) {
      logger.error(error);
    }
  });

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

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

⚠️ 同じ対象にマッチする正規表現の command() を複数使用する場合、マッチする 全ての リスナーが実行されることに注意してください。そのような挙動を意図しない場合は、これが発生しないよう正規表現をデザインしてください。

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

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

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

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

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

モーダルの開始

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// コマンド起動をリッスン
app.command('/ticket', async ({ ack, body, client, logger }) => {
  // コマンドのリクエストを確認
  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'
        }
      }
    });
    logger.info(result);
  }
  catch (error) {
    logger.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 ドキュメントを参照してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// action_id: button_abc のボタンを押すイベントをリッスン
// (そのボタンはモーダルの中にあるという想定)
app.action('button_abc', async ({ ack, body, client, logger }) => {
  // ボタンを押したイベントを確認
  await ack();

  try {
    if (body.type !== 'block_actions' || !body.view) {
      return;
    }
    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'
          }
        ]
      }
    });
    logger.info(result);
  }
  catch (error) {
    logger.error(error);
  }
});

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

view メソッドを使うと、ユーザーのビューとのインタラクションをリッスンすることができます。

ユーザーがモーダルからデータ送信したとき、Slack から view_submission のリクエストが送信されます。送信された input ブロックの値は state オブジェクトから取得できます。state 内には values というオブジェクトがあり、これは block_id と一意な action_id に紐づける形で入力値を保持しています。 モーダルでの notify_on_close プロパティを true に設定した場合、ユーザーが Close ボタンを押したときに Slack から view_closed リクエストが送信されます。 より詳細な情報は以下の モーダルを閉じるときのハンドリング を参照してください。 view_submissionview_closed リクエストをリッスンするには、組み込みの view() メソッドを使用できます。

view() メソッドでは、文字列か正規表現の callback_id の指定が必要です。typecallback_id を含む制約付きオブジェクトを渡すこともできます。


モーダル送信でのビューの更新

view_submission リクエストに対してモーダルを更新するには、リクエストの確認の中で update という response_action と新しく作成した view を指定します。

1
2
3
4
5
6
7
// モーダル送信でのビューの更新
app.view('modal-callback-id', async ({ ack, body }) => {
  await ack({
    response_action: 'update',
    view: buildNewModalView(body),
  });
});

この例と同様に、モーダルでの送信リクエストに対して、エラーを表示する ためのオプションもあります。

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


モーダルを閉じるときのハンドリング

💡 view_closed リクエストをリッスンするとき、callback_idtype: 'view_closed' を含むオブジェクトの指定が必要です。以下の例を参照してください。

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

1
2
3
4
5
6
// view_closed リクエストの処理
app.view({ callback_id: 'view_b', type: 'view_closed' }, async ({ ack, body, view, client }) => {
  // view_closed リクエストの確認
  await ack();
  // close リクエストについて何らかの処理
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// モーダルでのデータ送信リクエストを処理します
app.view('view_b', async ({ ack, body, view, client, logger }) => {
  // モーダルでのデータ送信リクエストを確認
  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) {
    logger.error(error);
  }

});

ホームタブの更新

ホームタブは、サイドバーや検索画面からアクセス可能なサーフェスエリアです。アプリはこのエリアを使ってユーザーごとのビューを表示することができます。アプリ設定ページで App Home の機能を有効にすると、views.publish API メソッドの呼び出しで user_idビューのペイロードを指定して、ホームタブを公開・更新することができるようになります。

エンドユーザーが App Home(ホームタブやアプリとの DM など)にアクセスしたことを知るために、app_home_opened イベントをサブスクライブすることができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// ユーザーが App Home にアクセスしたことを伝えるイベントをリッスン
app.event('app_home_opened', async ({ event, client, logger }) => {
  try {
    // 組み込みの API クライアントを使って views.publish を呼び出す
    const result = await client.views.publish({
      // イベントに紐づけられたユーザー ID を指定
      user_id: event.user,
      view: {
        // ホームタブはあらかじめアプリ設定ページで有効にしておく必要があります
        type: "home",
        blocks: [
          {
            type: "section",
            text: {
              type: "mrkdwn",
              text: "*Welcome home, <@" + event.user + "> :house:*"
            }
          },
          {
            type: "section",
            text: {
              type: "mrkdwn",
              text: "Learn how home tabs can be more useful and interactive <https://api.slack.com/surfaces/tabs/using|*in the documentation*>."
            }
          }
        ]
      }
    });

    logger.info(result);
  }
  catch (error) {
    logger.error(error);
  }
});

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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 of results) {
      options.push({
        text: {
          type: "plain_text",
          text: result.label
        },
        value: result.value
      });
    }

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

OAuth フローの実装

Slack アプリの配布を行うには Bolt による OAuth フローを実装し、インストール時に取得した情報をセキュアな方法で保存しておく必要があります。 Bolt は OAuth フローそのものに加えて OAuth のためのルーティング、 state パラメーターの検証、保存するためのインストール情報をアプリに受け渡す、などの処理をハンドリングします。

OAuth を有効にするために、以下を提供する必要があります:

  • clientId, clientSecret, stateSecret, scopes (必須)
  • installationStore オプションは、インストール情報の保存と取得を行うハンドラーを提供します (必須とはなっていませんが、本番環境では設定することを強く推奨します)
開発とテスト

開発・テストの際に利用することを想定して installationStore オプションのデフォルト実装である FileInstallationStore を提供しています。

1
2
3
4
5
6
7
8
9
10
const { App } = require('@slack/bolt');
const { FileInstallationStore } = require('@slack/oauth');
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:history', 'chat:write', 'commands'],
  installationStore: new FileInstallationStore(),
});

:warning: 本番運用での利用は 推奨しません ので、本番向けのデータストアはご自身で実装する必要があります。サンプルコードとして OAuth の他の実装例を参照してください。

アプリのインストール
  • インストールの開始: Bolt for JavaScript は /slack/install という インストール用のパス を生成します。これは、有効な state パラメータを生成した上で Slack アプリの直接のインストールを開始するための Add to Slack ボタンを含むページを応答する URL です。 www.example.com でホスティングされているアプリの場合、インストールページは www.example.com/slack/install となります。
    • 💡 App コンストラクタ内で installerOptions.directInstall: true を設定すると、デフォルトのウェブページを描画する代わりに、ユーザーを直接 Slack の authorize URL に誘導することができます()。
  • Add to Slack (Slack へ追加): Add to Slack ボタンを押すと Slack との OAuth プロセスを開始します。ユーザーがアプリへの権限付与を許可すると、Slack はアプリの Redirect URI (あらかじめ設定されています)へユーザーを誘導し、処理が正常に完了したらユーザーに Slack で開く よう促します。これらの設定をカスタマイズする方法については、後述の Redirect URI セクションを参照してください。

  • Slack で開く: ユーザーが Slack で開く を選択した後、アプリが Slack からのイベントをするときに installationStorefetchInstallationstoreInstallation ハンドラーが実行されます。ハンドラーに渡す引数に関するより詳しい情報は Installation Object セクションを参照してください。

  • アプリがすでにインストールされていて、さらにユーザーから追加の認可情報(例:ユーザートークンの発行)な場合や、何らかの理由で動的にインストール用の URL を生成したい場合は、ExpressReceiver を自前でインスタンス化し、それを receiver という変数に代入した上で receiver.installer.generateInstallUrl() を呼び出してください。詳しくは OAuth ライブラリのドキュメントgenerateInstallUrl() を参照してください。

  • 💡 Bolt for JavaScript は カスタムのレシーバーでの OAuth をサポートしていません。カスタムのレシーバーで OAuth フローを実装したい場合は、私たちが提供している OAuth ライブラリ を使うことができます。Bolt for JavaScript の組み込みのモジュールもこれを内部的に利用しています。
Redirect URI

Bolt for JavaScript は、アプリのインストールフローを完了した後の遷移先の URL である Redirect URI のためのパスとして /slack/oauth_redirect を有効にします。

💡 アプリのドメインを含んだ Redirect URI (絶対 URI)を Slack アプリの設定画面の OAuth and Permissions セクション内で設定してください。(例 https://example.com/slack/oauth_redirect )。

カスタムの Redirect URI を使う場合、 App クラスの引数 redirectUriinstallerOptions.redirectUriPath にも設定してください。 両方とも設定する必要があり、また、矛盾のないフル URI である必要があります。

1
2
3
4
5
6
7
8
9
10
11
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: ['chat:write'],
  redirectUri: 'https://example.com/slack/redirect', // ここに設定します
  installerOptions: {
    redirectUriPath: '/slack/redirect', // ここにも!
  },
});
Installation オブジェクト

Bolt は installationStorestoreInstallation ハンドラーに installation オブジェクトを渡します。どのようなオブジェクトの形式となるか想像しづらいと開発時に混乱の元になるかもしれません。installation オブジェクトはこのような形式となります:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
  team: { id: 'T012345678', name: 'example-team-name' },
  enterprise: undefined,
  user: { token: undefined, scopes: undefined, id: 'U01234567' },
  tokenType: 'bot',
  isEnterpriseInstall: false,
  appId: 'A01234567',
  authVersion: 'v2',
  bot: {
    scopes: [
      'chat:write',
    ],
    token: 'xoxb-244493-28*********-********************',
    userId: 'U012345678',
    id: 'B01234567'
  }
}

Bolt は fetchInstallationdeleteInstallation ハンドラーに installQuery オブジェクトを渡します:

1
2
3
4
5
6
7
{
  userId: 'U012345678',
  isEnterpriseInstall: false,
  teamId: 'T012345678',
  enterpriseId: undefined,
  conversationId: 'D02345678'
}
OrG 全体へのインストール

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

管理者画面からの Enterprise Grid の OrG 全体へのインストール の場合、 Bolt で動作させるために追加の設定が必要です。この利用シナリオでは、推奨の state パラメータが提供されず、Bolt アプリでは state を検証しようとするため、インストールを継続することができません。

Bolt アプリ側で stateVerification オプションを false に設定することで、 state パラメーターの検証を無効することができます。以下の例を参考にしてください。

1
2
3
4
5
6
7
8
9
const app = new App({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  clientId: process.env.SLACK_CLIENT_ID,
  clientSecret: process.env.SLACK_CLIENT_SECRET,
  scopes: ['chat:write'],
  installerOptions: {
    stateVerification: false,
  },
});

Slack の OAuth インストールフローについてのより詳細な情報は API ドキュメントを参照してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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 && installation.enterprise !== undefined) {
        // OrG 全体へのインストールに対応する場合
        return await database.set(installation.enterprise.id, installation);
      }
      if (installation.team !== undefined) {
        // 単独のワークスペースへのインストールの場合
        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');
    },
    deleteInstallation: async (installQuery) => {
      // 実際のデータベースから削除するために、ここのコードを変更
      if (installQuery.isEnterpriseInstall && installQuery.enterpriseId !== undefined) {
        // OrG 全体へのインストール情報の削除
        return await myDB.delete(installQuery.enterpriseId);
      }
      if (installQuery.teamId !== undefined) {
        // 単独のワークスペースへのインストール情報の削除
        return await myDB.delete(installQuery.teamId);
      }
      throw new Error('Failed to delete installation');
    },
  },
});

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

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

  • authVersion: 新しい Slack アプリとクラシック Slack アプリの切り替えに使用
  • metadata: セッションに関連する情報の指定に使用
  • installPath: “Add to Slack” ボタンのためのパスを変更するために使用
  • redirectUriPath: Redirect URL を変更するために使用
  • callbackOptions: OAuth フロー完了時の成功・エラー完了画面をカスタマイズするために使用
  • stateStore: 組み込みの ClearStateStore の代わりにカスタムのデータストアを有効にするために使用
  • userScopes: 親の階層にある scopes プロパティと同様、ユーザがアプリをインストールする際に必要となるユーザスコープのリストの指定に使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const database = {
  async get(key) {},
  async delete(key) {},
  async set(key, value) {}
};

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',
      userScopes: ['chat:write'],
      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 セクションから取得できます。

1
2
3
4
5
6
7
8
9
10
11
12
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 して、カスタムされたインスタンスとして定義することができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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');
})();

エラーの処理

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

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

また、様々なエラーパターンにより特化したエラーハンドラーを HTTPReceiver に直接設定することができます。

  • dispatchErrorHandler: 想定しないパスにリクエストが来たときに実行されます
  • processEventErrorHandler: リクエストを処理するとき(例:ミドルウェアや認可プロセス)に発生した例外に対して実行されます
  • unhandledRequestHandler: Slack からのリクエストが確認(ack())されなかったときに実行されます
  • unhandledRequestTimeoutMillis: リクエストが受信されてから unhandledRequestHandler が実行されるまでの待機時間(ミリ秒単位)。 デフォルトは 3001 です。

: あなたのアプリ内に定義されたカスタムのエラーハンドラーは、エラーとなった Slack からのリクストに応答するために response.writeHead() を呼び出して応答の HTTP ステータスコードを設定し、かつ response.end() を呼び出して Slack へのレスポンスを送信する必要があります。詳細は以下の例を参考にしてください。 </div>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { App, HTTPReceiver } from '@slack/bolt';

const app = new App({
  receiver: new HTTPReceiver({
    signingSecret: process.env.SLACK_SIGNING_SECRET,
    // より特定のパターンに特化したエラーハンドラー
    dispatchErrorHandler: async ({ error, logger, response }) => {
      logger.error(`dispatch error: ${error}`);
      response.writeHead(404);
      response.write("Something is wrong!");
      response.end();
    },
    processEventErrorHandler: async ({ error, logger, response }) => {
      logger.error(`processEvent error: ${error}`);
      // とにかく ack する
      response.writeHead(200);
      response.end();
      return true;
    },
    unhandledRequestHandler: async ({ logger, response }) => {
      logger.info('Acknowledging this incoming request because 2 seconds already passed...');
      // とにかく ack する
      response.writeHead(200);
      response.end();
    },
    unhandledRequestTimeoutMillis: 2000, // デフォルトは 3001
  }),
});

// より一般的なグローバルのエラーハンドラー
app.error(async (error) => {
  // メッセージ送信をリトライすべきか、アプリを停止すべきか判断するためにエラーの詳細を確認
  console.error(error);
});

エラーハンドラーでのさらなるデータの参照

グローバルエラーハンドラーの中で、リクエストからのデータをログ出力したい場合もあるでしょう。あるいは単に Bolt に設定した logger を利用したい場合もあるでしょう。

バージョン 3.8.0 からは、コンストラクターに extendedErrorHandler: true を渡すと、エラーハンドラーはリクエストの errorloggercontextbody を含むオブジェクトを受け取ります。

contextbody オブジェクト内にアクセスしたいプロパティが存在するかどうかをチェックすることをおすすめします。なぜなら body オブジェクト内に存在するデータはイベント毎に異なりますし、エラーはリクエストのライフサイクルの中のどんなタイミング(例えば context のプロパティが設定される前)でも発生しうるからです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const { App } = require('@slack/bolt');

const app = new App({
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  token: process.env.SLACK_BOT_TOKEN,
  extendedErrorHandler: true,
});

app.error(async ({ error, logger, context, body }) => {
  // Bolt で指定した logger を使ってエラー内容をログ出力
  logger.error(error);

  if (context.teamId) {
    // デバッグのために teamId を使ってなんらかの処理
  }
});

認可(Authorization)

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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 for JavaScript v3.5.0 から、アクセストークンのさらなるセキュリティ強化のレイヤーであるトークンローテーションの機能に対応しています。トークンローテーションは OAuth V2 の RFC で規定されているものです。

既存の Slack アプリではアクセストークンが無期限に存在し続けるのに対して、トークンローテーションを有効にしたアプリではアクセストークンが失効するようになります。リフレッシュトークンを利用して、アクセストークンを長期間にわたって更新し続けることができます。

Bolt for JavaScript の組み込みの OAuth 機能 を使用していれば、Bolt for JavaScript が自動的にトークンローテーションの処理をハンドリングします。

トークンローテーションに関する詳細は API ドキュメントを参照してください。


会話ストア

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 を実装することをおすすめします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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 マイグレーションガイドを参照してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
//  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 マイグレーションガイドを参照してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 'bot_message' サブタイプを持つメッセージをフィルタリングするリスナーミドルウェア
async function noBotMessages({ message, next }) {
  if (!message.subtype || message.subtype !== 'bot_message') {
    await next();
  }
}

// ボットではなく人間からのメッセージのみを受信するリスナー
app.message(noBotMessages, async ({ message, logger }) => logger.info(
  // 新規で投稿されたメッセージのみを処理
  if (message.subtype === undefined
    // || message.subtype === 'bot_message'
    || message.subtype === 'file_share'
    || message.subtype === 'thread_broadcast') {
    logger.info(`(MSG) User: ${message.user} Message: ${message.text}`)
  }
));

context の追加

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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, logger }) => {
  // コマンドリクエストの確認
  await ack();
  // リクエスト時のローカル時間を取得
  const localHour = (Date.UTC(2020, 3, 31) + context.tz_offset).getHours();

  // リクエストに使用するチャンネル ID
  const requestChannel = 'C12345';

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

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

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

ログの表示

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

1
2
3
4
5
6
7
8
9
// パッケージから 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 では、名前やレベルが無視され、すべてのメッセージがファイルに書き込まれることがあります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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: (level) => { },
    getLevel: () => { },
    setName: (name) => { },
  },
});

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

レシーバーは、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 のソースコードを参照してください。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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 ルートの追加

v3.7.0 から App を初期化する際に customRoutes というルートの配列を渡すことでカスタムの HTTP ルートを簡単に追加できるようになりました。

CustomRoute オブジェクトには pathmethodhandler という三つのプロパティが含まれていなければなりません。 HTTP メソッドに相当する method は文字列または文字列の配列です。

v3.13.0 からデフォルトの組み込みレシーバーである HTTPReceiverSocketModeReceiver が、Express.js が提供するものと同様な動的なルートパラメーターをサポートするようになりました。これによって URL 内に含まれる値を req.params の値として利用できるようになりました。

カスタムの HTTP ルートがローカル環境でどのポートからアクセスできるかを指定するために App コンストラクターに installerOptions.port というプロパティを渡すことができます。指定しない場合は、デフォルトの 3000 ポートとなります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const { App } = require('@slack/bolt');

// デフォルトの HTTPReceiver を使って Bolt アプリを初期化します
const app = new App({
  token: process.env.SLACK_BOT_TOKEN,
  signingSecret: process.env.SLACK_SIGNING_SECRET,
  customRoutes: [
    {
      path: '/health-check',
      method: ['GET'],
      handler: (req, res) => {
        res.writeHead(200);
        res.end(`Things are going just fine at ${req.headers.host}!`);
      },
    },
    {
      path: '/music/:genre',
      method: ['GET'],
      handler: (req, res) => {
        res.writeHead(200);
        res.end(`Oh? ${req.params.genre}? That slaps!`);
      },
    },
  ],
  installerOptions: {
    port: 3001,
  },
});

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

カスタム ExpressReceiver ルート

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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(...);
});

receiver.router.use((req, res, next) => {
  console.log(`Request time: ${Date.now()}`);
  next();
});

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

(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 アプリは対象のワークフローステップのイベントをリッスンしたり、設定オブジェクトが提供するコールバック関数を使ってイベントに応答したりすることができるようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 に設定します。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 は、デフォルトのステップのイメージ画像を上書きするために使用します

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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() 関数のいずれかを呼び出す必要があります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 });
    // 注意: processBeforeResponse: true を指定している場合
    // ここでは await complete() はおすすめしません。呼び出す API の応答が遅いためです。
    // これにより、3 秒以内に Slack のイベント API に応答することができなくなる場合があります。
    // 代わりに以下のようにすることができます:
    // complete({ outputs }).then(() => { console.log('workflow step execution complete registered'); });

    // もし何か問題が起きたら
    // fail({ error: { message: "Just testing step failure!" } }).then(() => { console.log('workflow step execution failure registered'); });
  },
});