Block Kitを用いたSlack通知をGoで実装してみる
この記事はフラー Advent Calendar 2020 の8日目の記事です。7日目は
@masaya82 さんで チェックボックスのアクセシビリティ対応をしたお話 - Qiita でした。
今年7月、フラー株式会社へ転職をいたしました。
現在はサーバサイドエンジニアとして働いており、主にGoを用いたウェブアプリケーションの開発を行っております。
その中でつい先日、Slack通知を行う機能を設ける必要が出てきました。
GoでのJSONの扱い方と合わせて諸々知見を得たので、今回はGoで実装するSlack投稿に関して書きたいと思います。
目指してる成果物
新型コロナウイルスの影響で殺伐とした社会になってしまった今だからこそ
Slackに癒し画像を投稿したいと思います。
さてSlackのメッセージを装飾する方法として、現状AttachmentsとBlock Kitの二つがあります。
前者のAttachmentsですが、どうやらレガシーなものになってしまっているようです(See Transforming your legacy message compositions with blocks | Slack)
そんなわけでBlock Kitを用いて構築していこうと思います。
通知は行いたいけどどんな見た目にしようか…?
そもそもどの程度の自由度あるの…?
そんな悩みを簡単に解決する術をSlackさんは提示してくださっております。
Slack Block Kit を用いた装飾をUIフレームワークを用いて簡単に作成できるものになります。
画面左側でBlockを選択すると、真ん中にイメージが追加され、右側にはJSON形式の出力がされる…うん、便利。
今回目指すべき投稿はこんな感じにしようかと
いいね、癒されますね。
癒し画像は、私の娘が愛してやまないふわたろうの画像を使用致します。
飼い主のタナイキさんにもご協力いただきました!ありがとうございます!
前準備
投稿先のSlackチャンネルとIncoming Webhookの準備
投稿する先のSlackチャンネルを準備してください。
またそのチャンネルにIncoming Webhookのアプリを追加してください。
該当チャンネルの詳細から「その他」>「アプリを追加する」からIncoming Webhookを選択すればOKです。
追加した際にWebhook URLが提示されるのでこれを使用します。
実装
では実装に入ります
ただ一つの文を送る機能の実装
とりあえず、Block Kit云々言う前に、ひとこと物申す機能を実装しようと思います。
先程取得したWebhook URLにBodyにJSONを持ったPOSTを投げればできます。
肝心のJSONデータはこんな感じ
{ "username" : "通知上で表示されるユーザ名", "icon_url" : "通知上で表示されるユーザ画像", // icon_emoji で指定するとSlack絵文字が使用できます "text" : "通知内容" }
ではこれを送るコードを実装しましょう
package main import ( "bytes" "encoding/json" "fmt" "io" "io/ioutil" "net/http" ) func main() { // 投稿先SlackURL slack := "https://投稿先SlackURL" // BodyのJSONの準備 bodyJSON, err := json.Marshal(map[string]interface{}{ "username": "inoriko711", "icon_url": "https://iconURL", "text": "テスト", }) if err != nil { fmt.Println(err) return } // POST通信の実施 resp, err := http.Post(slack, "application/json", bytes.NewReader(bodyJSON)) if err != nil { fmt.Println(err) return } defer resp.Body.Close() // responce Bodyを最後まで読んでエラーがないことの確認 if _, err := io.Copy(ioutil.Discard, resp.Body); err != nil { fmt.Println(err) } }
go run [ファイル名].go で実行!
…でけた
Block Kitの箇所を準備する
Block Kit Builderで作成されたJSONですが、思いの外複雑な上、必須パラメータが欠けると問答無用で投稿できなくなります。
(Blockが欠けるとかじゃないです。全て投稿できなくなるんです。)
さらに、私、自慢じゃないですけどtypoが多いです。いちいちJSONのキー名を手打ちするのはしんどいです。
そのため各Block毎に構造体を作成して、その構造体に沿ってさえいれば問題なく投稿できるような仕組みを準備します。
HeaderのBlockを準備する
Header部分はこちら
該当JSONはこんな感じです。
{ "type": "header", "text": { "type": "plain_text", "text": ":star2:癒しタイム:star2:", "emoji": true } }
Headerの要素に関する詳しい説明はこちらReference: Layout blocks | Slackに書かれてあります。
Fieldとしてはtypeとtextが必須、block_idは任意。型としてはtypeとblock_idはstring、textはオブジェクトです。
そのためGoコードとしては以下の型を準備します。
type HeaderBlock struct { Type string `json:"type"` Text interface{} `json:"text"` BlockID string `json:"block_id,omitempty"` }
`json:"キー名"で入力しておくことで、Go側でよしなにしてくれます。便利。
またomitemptyを付与しておくことでコード上で何も指定されなかった(nilの)際、JSONに直すときにキー毎省いてくれます。とっても便利。
先程textの値の型はオブジェクトだって申し上げた舌の根も乾かぬうちにアレですが、よくよく読むと値はTextObjectしか持たないようです。
と言うわけで
TextObjectの型を準備して、先程準備したHeaderBlockのTextの型を*TextObjectにしてしまいましょう。
type TextObject struct { Type string `json:"type"` Text string `json:"text"` Emoji bool `json:"emoji,omitempty"` Verbatim bool `json:"verbatim,omitempty"` } type HeaderBlock struct { Type string `json:"type"` Text *TextObject `json:"text"` BlockID string `json:"block_id,omitempty"` }
今回は特にBlockIDやVerbatimを使用する予定はないのですが、今後のことを考えて実装してしまいます。
以下同じようなことを繰り返すので、十分わかったよって人は飛ばしちゃってください。
Sectionのplain textブロックを準備する
「本日の癒し」の箇所です。シンプルそうですね。
こちらを参考に作ります
type SectionBlocks struct { Type string `json:"type"` Text *TextObject `json:"text"` BlockID string `json:"block_id,omitempty"` Fields string `json:"fields,omitempty"` Accessory interface{} `json:"accessory,omitempty"` }
…あまりシンプルじゃなかった
Imageのnotilteブロックを準備する
お待ちかねのふわたろうの画像の箇所です!
こちらを参考に作ります
type ImageBlock struct { Type string `json:"type"` ImageURL string `json:"image_url"` AltText string `json:"alt_text"` Title *TextObject `json:"title,omitempty"` BlockID string `json:"block_id,omitempty"` }
ActionsのButtonブロックを準備する
↓この箇所です。ボタンを押すとそれぞれのTwitterへ遷移します。
こちらを参考に作ります。
type ActionBlock struct { Type string `json:"type"` Elements []interface{} `json:"elements"` BlockID string `json:"block_id,omitempty"` }
またelementsとしてButton elementを使用するため、こちらの構造体も準備します。
type ButtonElement struct { Type string `json:"type"` Text interface{} `json:"text"` ActionId string `json:"action_id,omitempty"` URL string `json:"url,omitempty"` Value string `json:"value,omitempty"` Style string `json:"style,omitempty"` Confirm interface{} `json:"confirm,omitempty"` }
Contextのtext and imagesブロックを準備する
一番下のブロックです。
こちらを参考に作ります。
type ContextBlock struct { Type string `json:"type"` Elements []interface{} `json:"elements"` BlockID string `json:"block_id,omitempty"` }
準備した構造体に値を突っ込む
長々と準備しました構造体に値をセットします。
func buildBlocks() []interface{} { return []interface{}{ &HeaderBlock{ Type: "header", Text: &TextObject{ Type: "plain_text", Text: ":star2:癒しタイム:star2:", }, }, &SectionBlocks{ Type: "section", Text: &TextObject{ Type: "mrkdwn", Text: "本日の癒し", }, }, &ImageBlock{ Type: "image", ImageURL: "https://imageURL", AltText: "fuwataro", }, &ActionBlock{ Type: "actions", Elements: []interface{}{ &ButtonElement{ Type: "button", Text: &TextObject{ Type: "plain_text", Text: "ふわたろう", Emoji: true, }, URL: "https://twitter.com/huwataro_", Value: "fuwataro Twitter", }, &ButtonElement{ Type: "button", Text: &TextObject{ Type: "plain_text", Text: "inoriko", Emoji: true, }, URL: "https://twitter.com/inoriko711", Value: "inoriko Twitter", }, &ButtonElement{ Type: "button", Text: &TextObject{ Type: "plain_text", Text: "Fuller, Inc.", Emoji: true, }, URL: "https://twitter.com/fuller_inc", Value: "fuller Twitter", }, }, }, &ContextBlock{ Type: "context", Elements: []interface{}{ &ImageBlock{ Type: "image", ImageURL: "https://imageURL", AltText: "inoriko711", }, &TextObject{ Type: "plain_text", Text: "Author: inoriko711", Emoji: true, }, }, }, } }
実行してみる
body JSONにblocks要素を付与。
// BodyのJSONの準備 bodyJSON, err := json.Marshal(map[string]interface{}{ "username": "inoriko711", "icon_url": "https://iconURL", "text": "癒し画像のお届け", "blocks": blocks, })
go run 実行!!!!
投稿できました。
投稿内容を可変にする
折角作った投稿機能、より汎用性を高めるべく、以下の値を可変にしようと思います。
- Slack通知先
- 通知アプリアイコン画像
- 通知アプリユーザ名
また毎度同じ内容を投稿し続けるのも気が引けます。
そのため以下の値も可変にしようと思います。
- 通知本文
- 癒し画像
- 癒し画像タイトル
- 投稿者画像
- 投稿者画像タイトル
- 投稿者名
実装
本当であれば編集画面を準備してあれこれやりたいのですが、残念ながら今回そんな余力はありませんでした。
今回のところは可変データを保持するJSONファイルを準備して、それを読み込んであれこれするに留めておきます。
準備したJSONデータ
{ "slack_url": "投稿先SlackURL", "username": "可変テストユーザ名", "icon_url": "通知で用いるアイコン", "slack_notice_data": { "text": "通知本文", "healing_image_url": "癒し画像URL", "healing_image_text": "癒し画像タイトル", "author_image_url": "投稿者画像URL", "author_image_text": "投稿者画像タイトル", "author_name": "投稿者名" } }
JSONを扱うので、例によって構造体の準備
// Slack通知可変箇所のデータ構造体 type SlackApp struct { SlackURL string `json:"slack_url"` Username string `json:"username"` IconURL string `json:"icon_url"` SlackNoticeDataType *SlackNoticeData `json:"slack_notice_data"` } // Slack投稿内容可変箇所のデータ構造体 type SlackNoticeData struct { Text string `json:"text"` HealingImageURL string `json:"healing_image_url"` HealingImageText string `json:"healing_image_text"` AuthorImageURL string `json:"author_image_url"` AuthorImageText string `json:"author_image_text"` AuthorName string `json:"author_name"` }
そしてJSONファイルを読み込んで値を取得する箇所
// ファイルから読み込む dataJSON, err := ioutil.ReadFile("./data/data.json") if err != nil { fmt.Println(err) return } // ファイルから読み込んだデータをSlackApp型で持つ var data SlackApp err = json.Unmarshal(dataJSON, &data) if err != nil { fmt.Println(err) return }
あとは可変にしたい箇所に、読み込んだ値を充てていけば完成です!
実行
良き
おまけ
今日お誕生日の人、おめでとうございます。