Goで作るSlack Modal Applicationサンプルコード 1/4
どんな記事?
- Goで作るSlack modal applicationのサンプルコードをご紹介
- ポップアップで画面が飛び出してくるアレです
- 基本機能をほぼ入れ込みましたので、forkして自由にカスタムください!
Modal Applicationとは
一言でいうと、複数の画面遷移を伴うアプリでも、Slack上でラクに実装できちゃう仕組み。
僕もいくつか自作のアプリをチームのSlackに組み込んでるんですが、まぁこれが便利で便利で。
- テキストボックス
- 選択リスト
- ラジオボタン
- チェックボックス
と、ひととおりのUIコンポーネントが揃っているので、日常使うチーム内アプリぐらいならだいたい作れるようになってます。
今回は、これからModalアプリを作ってみようと考えられてるあなたに向けて、まだまだ実装例も少ないModalのサンプルコードをご紹介したいと思います。
今回のサンプル
今回作成するのは「出前アプリ」。何はともあれまずは完成形を見てみましょう。
ざっくりと、
- Botを呼んで
- お店を選ぶボタンを押すと
- ダイアログ(Modal)がポップアップするので、注文を入力
- 支払い確認画面でOKを押すと
- 完了通知が飛んでくる
という仕様。Modalを使ったアプリの基本である
- 呼び出し
- トリガーメッセージ送信
- Modal新規作成
- Modal更新
- バリデーション
- 通知送信
をすべて網羅した作りになっているので、これをベースにカスタムしていただければと思います。
処理の流れ
出前アプリの処理内容をStep別にまとめてみました。
- STEP1: Botを呼び出して、トリガーメッセージを送付
- STEP2: お店情報のModalを送付
- STEP3: 注文確認画面のModalを送付
- STEP4: 完了メッセージを送付
なかなかに長い処理となるので、まずは「やりとりは4回するんだな」くらいに覚えてもらえばOKです。
では、STEP別にみていきましょう!
STEP1 Botを呼び出して、トリガーメッセージを送付
最初のSTEPで行うのは、
- リクエストの認証(自分のWorkspaceから送付されたメッセージか)
- リクエストの認証(呼び出し元イベントがなにか)
- トリガーメッセージの送信
の3つです。
「ん?トリガーメッセージ送付?Modalじゃなくて?」と思われた方、そのとおり。はじめはModalでなくメッセージを送るんです。
なんでこんなことになっているかというと、ModalはSlackの仕様上、「Interactive message」つまり 「ボタンやセレクトボックスのついたメッセージ」からしか開けないつくりになっています。
Modalをはじめから送れないので、まずはメッセージを送る作りにしてるんですね。
また、
- はじめのBot呼び出し(STEP1)
- 以降の処理(STEP2〜4)
で、リクエストが送付されてくるURLが異なるのもポイント。サーバーサイドには受口を2つ作っておきましょう。
STEP1の完成イメージはこんな感じ。
コアとなるhandlerのソースはこちらです。(各関数の実装含めた完全版は、GitHub参照)
func handleEventRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
// Verify the request.
if err := verify(request, signingSecret); err != nil {
log.Printf("[ERROR] Failed to verify request: %v", err)
return events.APIGatewayProxyResponse{StatusCode: 200}, nil
}
// Parse event.
eventsAPIEvent, err := slackevents.ParseEvent(json.RawMessage(request.Body), slackevents.OptionNoVerifyToken())
if err != nil {
log.Printf("[ERROR] Failed to parse request body: %v", err)
return events.APIGatewayProxyResponse{StatusCode: 200}, nil
}
// Check if the request type is URL Verification. This logic is only called from slack developer's console when you set up your app.
if eventsAPIEvent.Type == slackevents.URLVerification {
var r *slackevents.ChallengeResponse
if err := json.Unmarshal([]byte(request.Body), &r); err != nil {
log.Printf("[ERROR] Failed to unmarshal json: %v", err)
return events.APIGatewayProxyResponse{StatusCode: 200}, nil
}
return events.APIGatewayProxyResponse{Body: r.Challenge, StatusCode: 200}, nil
}
// Verify the request type.
if eventsAPIEvent.Type != slackevents.CallbackEvent {
log.Printf("[ERROR] Unexpected event type: expect = CallbackEvent , actual = %v", eventsAPIEvent.Type)
return events.APIGatewayProxyResponse{StatusCode: 200}, nil
}
// Verify the event type.
switch ev := eventsAPIEvent.InnerEvent.Data.(type) {
case *slackevents.AppMentionEvent:
// Create a shop list.
list := createShopListBySDK()
// Send a shop list to slack channel.
api := slack.New(tokenBotUser)
if _, _, err := api.PostMessage(ev.Channel, list); err != nil {
log.Printf("[ERROR] Failed to send a message to Slack: %v", err)
return events.APIGatewayProxyResponse{StatusCode: 200}, nil
}
default:
return events.APIGatewayProxyResponse{StatusCode: 200}, nil
}
return events.APIGatewayProxyResponse{StatusCode: 200}, nil
}
では、ポイントをみていきます。
リクエストの認証(自分のWorkspaceから送付されたメッセージか)
はじめの認証では、メッセージが自分のWorkspaceから来ていることを確認します。
slack-go/slackに認証用の関数が用意されているので、それを使いましょう。
リクエストbody、header、ポータルから発行できるSigning Secretを渡します。
この部分は、すべてのSTEPで利用するので、関数化しておくのがおすすめです。
// verify returns the result of slack signing secret verification.
func verify(request events.APIGatewayProxyRequest, sc string) error {
body := request.Body
header := http.Header{}
for k, v := range request.Headers {
header.Set(k, v)
}
sv, err := slack.NewSecretsVerifier(header, sc)
if err != nil {
return err
}
sv.Write([]byte(body))
return sv.Ensure()
}
メッセージ認証(呼び出し元イベントがなにか)
次の認証では、リクエストがなにをトリガーに呼ばれたものかを確認します。
まずは、リクエストをParse。
// Parse event.
eventsAPIEvent, err := slackevents.ParseEvent(json.RawMessage(request.Body), slackevents.OptionNoVerifyToken())
if err != nil {
log.Printf("[ERROR] Failed to parse request body: %v", err)
return events.APIGatewayProxyResponse{StatusCode: 200}, nil
}
しれっと失敗時に200OKを返していますが、ここ重要です。
Slack APIのルールでは、リクエストを受け取れたら200OKを返してね! と書かれているため、サーバー側のロジックで失敗した場合にも200OKを返すようにします。
意識していないと、5XXや4XXを返しがちなので注意しましょう。
さて、Parseが成功したら次は認証です。今回はBotからの呼び出しであることを確認していますが、必要に応じてロジック追加してください。
// Verify the request type.
if eventsAPIEvent.Type != slackevents.CallbackEvent {
log.Printf("[ERROR] Unexpected event type: expect = CallbackEvent , actual = %v", eventsAPIEvent.Type)
return events.APIGatewayProxyResponse{StatusCode: 200}, nil
}
// Verify the event type.
switch ev := eventsAPIEvent.InnerEvent.Data.(type) {
case *slackevents.AppMentionEvent:
// ここにトリガーメッセージ送付処理
default:
return events.APIGatewayProxyResponse{StatusCode: 200}, nil
}
よくあるのは、、呼び出し元チャンネルや呼び出しユーザを確認するパターンかなあと。
特にプロダクション環境を触れるようなアプリを作る場合は、ガチガチに固めておきましょう。
トリガーメッセージ送付
最後にトリガーメッセージを送ります。
Slackが公式にGUIツール「Block Kit Builder」を用意してくれているので、そちらで見た目を確認しながら作りましょう。
func createShopListBySDK() slack.MsgOption {
// Top text
descText := slack.NewTextBlockObject("mrkdwn", "What do you want to have?", false, false)
descTextSection := slack.NewSectionBlock(descText, nil, nil)
// Divider
dividerBlock := slack.NewDividerBlock()
// Shops
// - Hamburger
hamburgerButtonText := slack.NewTextBlockObject("plain_text", "Order", true, false)
hamburgerButtonElement := slack.NewButtonBlockElement("actionIDHamburger", "hamburger", hamburgerButtonText)
hamburgerAccessory := slack.NewAccessory(hamburgerButtonElement)
hamburgerSectionText := slack.NewTextBlockObject("mrkdwn", ":hamburger: *Hungryman Hamburgers*\nOnly for the hungriest of the hungry.", false, false)
hamburgerSection := slack.NewSectionBlock(hamburgerSectionText, nil, hamburgerAccessory)
// - Sushi
sushiButtonText := slack.NewTextBlockObject("plain_text", "Order", true, false)
sushiButtonElement := slack.NewButtonBlockElement("actionIDSushi", "sushi", sushiButtonText)
sushiAccessory := slack.NewAccessory(sushiButtonElement)
sushiSectionText := slack.NewTextBlockObject("mrkdwn", ":sushi: *Ace Wasabi Rock-n-Roll Sushi Bar*\nFresh raw wish and wasabi.", false, false)
sushiSection := slack.NewSectionBlock(sushiSectionText, nil, sushiAccessory)
// - Ramen
ramenButtonText := slack.NewTextBlockObject("plain_text", "Order", true, false)
ramenButtonElement := slack.NewButtonBlockElement("actionIDRamen", "ramen", ramenButtonText)
ramenAccessory := slack.NewAccessory(ramenButtonElement)
ramenSectionText := slack.NewTextBlockObject("mrkdwn", ":ramen: *Sazanami Ramen*\nWhy don't you try Japanese soul food?", false, false)
ramenSection := slack.NewSectionBlock(ramenSectionText, nil, ramenAccessory)
// Blocks
blocks := slack.MsgOptionBlocks(descTextSection, dividerBlock, hamburgerSection, sushiSection, ramenSection)
return blocks
}
作成〜送信まで、上記の関数を使ったhandlerの実装は以下の部分です。
// Create a shop list.
list := createShopListBySDK()
// Send a shop list to slack channel.
api := slack.New(tokenBotUser)
if _, _, err := api.PostMessage(ev.Channel, list); err != nil {
log.Printf("[ERROR] Failed to send a message to Slack: %v", err)
return events.APIGatewayProxyResponse{StatusCode: 200}, nil
}
ここまでで、STEP1は終了です!
Discussion