アプリが受信可能なメッセージをリッスンするには、message
型でないイベントを除外する message()
メソッドを使用します。
message()
は、string
型か RegExp
型の、指定パターンに一致しないメッセージを除外する pattern
パラメーター(指定は必須ではありません)を受け付けます。
アプリが受信可能なメッセージをリッスンするには、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 の一致結果はすべて 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_changed
や message_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 メソッドを呼び出すには、リスナー関数の引数に 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_id
、 block_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()
は response_url
を呼び出すためのユーティリティであるため、それを直接使うときと同様に動作します。新しいメッセージのペイロードと、オプショナルな引数である response_type
(値は in_channel
または ephemeral
)、 replace_original
、 delete_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-]{2,4}$/
// 制約付きのオブジェクト を使用して 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_id
や type
によるリスニングができます。オブジェクト内の制約は文字列型または 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
を渡します。ユーザーが既存のモーダル内の要素とインタラクションを行なった(例:ボタンを押す、メニューから選択する)ことをトリガーにビューを更新する場合、そのリクエストの body
に view_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_submission
や view_closed
リクエストをリッスンするには、組み込みの view()
メソッドを使用できます。
view()
メソッドでは、文字列か正規表現の callback_id
の指定が必要です。type
と callback_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_id
と type: '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();
}
});
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(),
});
本番運用での利用は 推奨しません ので、本番向けのデータストアはご自身で実装する必要があります。サンプルコードとして OAuth の他の実装例を参照してください。
/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 からのイベントをするときに installationStore
の fetchInstallation
や storeInstallation
ハンドラーが実行されます。ハンドラーに渡す引数に関するより詳しい情報は Installation Object セクションを参照してください。
アプリがすでにインストールされていて、さらにユーザーから追加の認可情報(例:ユーザートークンの発行)な場合や、何らかの理由で動的にインストール用の URL を生成したい場合は、ExpressReceiver
を自前でインスタンス化し、それを receiver
という変数に代入した上で receiver.installer.generateInstallUrl()
を呼び出してください。詳しくは OAuth ライブラリのドキュメントの generateInstallUrl()
を参照してください。
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 クラスの引数 redirectUri
と installerOptions.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', // ここにも!
},
});
Bolt は installationStore
の storeInstallation
ハンドラーに 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 は fetchInstallation
と deleteInstallation
ハンドラーに installQuery
オブジェクトを渡します:
1
2
3
4
5
6
7
{
userId: 'U012345678',
isEnterpriseInstall: false,
teamId: 'T012345678',
enterpriseId: undefined,
conversationId: 'D02345678'
}
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');
},
},
});
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: true
と appToken: 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
を渡すと、エラーハンドラーはリクエストの error
、 logger
、 context
、 body
を含むオブジェクトを受け取ります。
context
や body
オブジェクト内にアクセスしたいプロパティが存在するかどうかをチェックすることをおすすめします。なぜなら 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)は、Slack からのリクエストを処理するにあたって、どの Slack クレデンシャル (ボットトークンなど) を使用可能にするかを決定するプロセスです。
1 つだけのワークスペースにインストールされたカスタムアプリであれば App
初期化時に単に token
オプションを使用するだけで OK です。一方で、複数のワークスペースにインストールされる、複数のユーザートークンを使用するといったケースのように、アプリが複数のトークンを処理しなければならない場合があります。このようなケースでは token
の代わりに authorize
オプションを使用する必要があります。
authorize
オプションには、イベントソースを入力値として受け取り、許可された認可されたクレデンシャルを含むオブジェクトを Promise の値として返す関数を指定します。このイベントソースの情報には、 teamId
(常に存在します)、 userId
、conversationId
、enterpriseId
のような、リクエストが誰によって発生させられたか、どこで発生したかに関する情報が含まれます。
許可されたクレデンシャルには、botToken
、userToken
、botId
(アプリがボット自体からのメッセージを無視するために必要です)、 botUserId
が含まれます。context
オブジェクトに、これ以外の他のプロパティを自由に設定することもできます。
botToken
と userToken
は、どちらか、またはその両方を必ず設定してください。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
オブジェクトは、受信リクエストに付加情報を提供するために使用されるもので、全てのリスナーがこれを使用できます。例えば、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
を指定して、カスタマイズできます。使用可能なログレベルは、頻度の高い方から順に、DEBUG
、INFO
、WARN
、ERROR
です。
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);
}
}
v3.7.0
から App
を初期化する際に customRoutes
というルートの配列を渡すことでカスタムの HTTP ルートを簡単に追加できるようになりました。
各 CustomRoute
オブジェクトには path
、 method
、 handler
という三つのプロパティが含まれていなければなりません。 HTTP メソッドに相当する method
は文字列または文字列の配列です。
v3.13.0
からデフォルトの組み込みレシーバーである HTTPReceiver
と SocketModeReceiver
が、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');
})();
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
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();
console.log('⚡️ Bolt app started'');
})();
(アプリによる)ワークフローステップ(Workflow Steps from Apps) は、ワークフロービルダーにおけるワークフローに組み込み可能なカスタムのワークフローステップを任意の Slack アプリが提供することを可能とします。
ワークフローステップは、三つの異なるユーザーイベントから構成されます:
ワークフローステップを機能させるためには、これら三つのイベント全てを適切にハンドリングする必要があります。
ワークフローステップのさらなる詳細については API ドキュメントを参考にしてください。
ワークフローステップを作るための手段として Bolt は WorkflowStep
というクラスを提供しています。
新しい WorkflowStep
インスタンスの生成には、そのステップの callback_id
と設定オブジェクトを渡します。
設定オブジェクトには edit
、save
、execute
という三つのプロパティがあります。これらのそれぞれは単一のコールバック関数、またはコールバック関数の配列である必要があります。すべてのコールバック関数は、ワークフローステップのイベントに関する情報を保持しする 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'); });
},
});