💬

Goで作るSlack Modal Applicationサンプルコード 3/4

2020/09/25に公開

STEP3 注文確認画面のModalを送る

つづいて、注文確認画面の作成と送付。STEP2で作った注文Modalの入力情報をもとに作成します。

完成イメージはこんな感じ。

今回は、注文内容の確認だけでなく、Chipを入れるフィールドも作ってみました。せっかくなので、これを使ってバリデーションの方法も学習しておきましょう(詳細はSTEP4で!)。

このSTEPでやることは、

  • リクエストの認証(自分のWorkspaceからのリクエストかどうか)
  • リクエストをStructにマッピング
  • 注文内容を取得
  • Modalを作成して送信

の4つです。リクエストの認証と、StructにマッピングはSTEP2と同様なので省きます。

handlerのソースはこちら。(各関数の実装含めた完全版は、GitHub参照)

func handleOrderSubmissionRequest(message slack.InteractionCallback) (events.APIGatewayProxyResponse, error) {
	// Get the selected information.
	// - radio button
	menu := message.View.State.Values["block_id_menu"]["action_id_menu"].SelectedOption.Value

	// - static_select
	steak := message.View.State.Values["block_id_steak"]["action_id_steak"].SelectedOption.Value

	// - text
	note := message.View.State.Values["block_id_note"]["action_id_note"].Value

	// Create a confirmation modal.
	// - apperance
	modal := createConfirmationModalBySDK(menu, steak, note)

	// - metadata : CallbackID
	modal.CallbackID = reqConfirmationModalSubmission

	// - metadata : ExternalID
	modal.ExternalID = message.User.ID + strconv.FormatInt(time.Now().UTC().UnixNano(), 10)

	// - metadata : PrivateMeta
	//   - Get private metadata of a message
	var pMeta privateMeta
	if err := json.Unmarshal([]byte(message.View.PrivateMetadata), &pMeta); err != nil {
		return events.APIGatewayProxyResponse{StatusCode: 200}, fmt.Errorf("failed to unmarshal private metadata: %w", err)
	}

	//   - Create new private metadata
	params := privateMeta{
		ChannelID: pMeta.ChannelID,
		order: order{
			Menu:   menu,
			Steak:  steak,
			Note:   note,
			Amount: "700",
		},
	}

	pBytes, err := json.Marshal(params)
	if err != nil {
		return events.APIGatewayProxyResponse{StatusCode: 200}, fmt.Errorf("failed to marshal private metadata: %w", err)
	}

	modal.PrivateMetadata = string(pBytes)

	// Create response
	resAction := slack.NewUpdateViewSubmissionResponse(modal)
	rBytes, err := json.Marshal(resAction)
	if err != nil {
		return events.APIGatewayProxyResponse{StatusCode: 200}, fmt.Errorf("failed to marshal json: %w", err)
	}

	return events.APIGatewayProxyResponse{
		StatusCode:      200,
		IsBase64Encoded: false,
		Headers: map[string]string{
			"Content-Type": "application/json",
		},
		Body: string(rBytes),
	}, nil
}

では、ポイントです。

注文内容を取得

Modalへの入力情報は、messageのView.Stateフィールドにmap形式で格納されているので、block_id、action_idをkeyに取り出します。

// Get the selected information.
// - radio button
menu := message.View.State.Values["block_id_menu"]["action_id_menu"].SelectedOption.Value

// - static_select
steak := message.View.State.Values["block_id_steak"]["action_id_steak"].SelectedOption.Value

// - text
note := message.View.State.Values["block_id_note"]["action_id_note"].Value

input blockの種類によって、取り出し方が微妙に違うので注意。

Modalを作成して送信

Modalの作成手順は基本的にSTEP2と同様ですが、今回は注文内容に合わせて、動的に内容を変えるためSDKでの作成しかできない点に注意。

(一応強引にJSONから作成もできます。JSON内にPlace Holderを置いておいて、strings.Replaceで入力値に置き換える、だったりをすれば可能は可能です。)

func createConfirmationModalBySDK(menu, steak, note string) *slack.ModalViewRequest {

	// Create a modal.
	// - Text section
	titleText := slack.NewTextBlockObject("mrkdwn", ":wave: *Order confirmation*", false, false)
	titleTextSection := slack.NewSectionBlock(titleText, nil, nil)

	// Divider
	dividerBlock := slack.NewDividerBlock()

	// - Text section
	sMenuText := slack.NewTextBlockObject("mrkdwn", "*Menu :hamburger:*\n"+burgers[menu], false, false)
	sMenuTextSection := slack.NewSectionBlock(sMenuText, nil, nil)

	// - Text section
	sSteakText := slack.NewTextBlockObject("mrkdwn", "*How do you like your steak?*\n"+steak, false, false)
	sSteakTextSection := slack.NewSectionBlock(sSteakText, nil, nil)

	// - Text section
	sNoteText := slack.NewTextBlockObject("mrkdwn", "*Anything else you want to tell us?*\n"+note, false, false)
	sNoteTextSection := slack.NewSectionBlock(sNoteText, nil, nil)

	// - Text section
	amountText := slack.NewTextBlockObject("mrkdwn", "*Amount :moneybag:*\n$ 700", false, false)
	amountTextSection := slack.NewSectionBlock(amountText, nil, nil)

	// - Input with plain_text_input
	chipText := slack.NewTextBlockObject("plain_text", "Chip ($)", false, false)
	chipInputElement := slack.NewPlainTextInputBlockElement(nil, "action_id_chip")
	chipInput := slack.NewInputBlock("block_id_chip", chipText, chipInputElement)
	chipHintText := slack.NewTextBlockObject("plain_text", "Thank you for your kindness!", false, false)
	chipInput.Hint = chipHintText
	chipInput.Optional = true

	// Blocks
	blocks := slack.Blocks{
		BlockSet: []slack.Block{
			titleTextSection,
			dividerBlock,
			sMenuTextSection,
			sSteakTextSection,
			sNoteTextSection,
			dividerBlock,
			amountTextSection,
			chipInput,
		},
	}

	// ModalView
	modal := slack.ModalViewRequest{
		Type:   slack.ViewType("modal"),
		Title:  slack.NewTextBlockObject("plain_text", "Hungryman Hamburgers", false, false),
		Close:  slack.NewTextBlockObject("plain_text", "Cancel", false, false),
		Submit: slack.NewTextBlockObject("plain_text", "Order!", false, false),
		Blocks: blocks,
	}

	return &modal
}

つづいて、metadataの追加。先程と同様にCallbackID、ExternalID、Private Metadataを追加します。

CallbackID

// - metadata : CallbackID
modal.CallbackID = reqConfirmationModalSubmission

ExternalID

こちらもユーザーID+タイムスタンプから変化なし。

// - metadata : ExternalID
modal.ExternalID = message.User.ID + strconv.FormatInt(time.Now().UTC().UnixNano(), 10)

Private Metadata

Private Metadataには、

  • チャンネルID
  • 注文内容

の2つを保管しましょう。繰り返しになりますが、Modalアプリの各通信はステートレスなので、次の通信に引き継ぎたい情報はPrivate Metadataに自分で入れる必要があります。

まずは、注文ModalのPrivate Metadataを取り出します。

// - metadata : PrivateMeta
//   - Get private metadata of a message
var pMeta privateMeta
if err := json.Unmarshal([]byte(message.View.PrivateMetadata), &pMeta); err != nil {
    return events.APIGatewayProxyResponse{StatusCode: 200}, fmt.Errorf("failed to unmarshal private metadata: %w", err)
}

次にPrivate Metadataから取り出したチャンネルIDと、message.View.State内の注文内容から新たにPrivate Metadataを生成。注文確認Modalに埋め込みましょう。

//   - Create new private metadata
params := privateMeta{
    ChannelID: pMeta.ChannelID,
    order: order{
        Menu:   menu,
        Steak:  steak,
        Note:   note,
        Amount: "700",
    },
}

pBytes, err := json.Marshal(params)
if err != nil {
    return events.APIGatewayProxyResponse{StatusCode: 200}, fmt.Errorf("failed to marshal private metadata: %w", err)
}
modal.PrivateMetadata = string(pBytes)

ここまでできたら、あとは送信するのみ。

今回はViewの更新なので、NewUpdateViewSubmissionResponseを使って、レスポンスとして返してあげればOKです。

// Create response
resAction := slack.NewUpdateViewSubmissionResponse(modal)
rBytes, err := json.Marshal(resAction)
if err != nil {
    return events.APIGatewayProxyResponse{StatusCode: 200}, fmt.Errorf("failed to marshal json: %w", err)
}

return events.APIGatewayProxyResponse{
    StatusCode:      200,
    IsBase64Encoded: false,
    Headers: map[string]string{
        "Content-Type": "application/json",
    },
    Body: string(rBytes),
}, nil

Discussion