去年の12月に何故か急に Go で slack bot を作りたくなったんだが、急にやる気がなくなって放置してて
無料トライアル枠なくなるやーんと焦ったので、作ることにする。
ちなみに一番最初は AppEngine でやろうかなと考えてたが、そんな大層なものを作るわけでもなかったので Cloud Function で作り直すことにした。
なお、ほとんどこれの Go 版だと思ってもらえばいい。
環境
現在 Cloud Function でサポートされている Go のバージョンは1.11。 homebrew で素直にインストールすると Go 1.13 になってしまうので
を用いるなどして Go 1.11 を使うことを推奨する。
特に Go 1.11 にはあって Go 1.13 にはない機能は使ってないが再現したいのであれば可能な限り同じバージョンのほうがいい。
とりあえず slack からの post message に反応できるようにする
上記 Qiita の Cloud Functions の準備 に当たる関数を Go で用意すると以下になる。( function.go というファイル名とする。)
package sample import ( "encoding/json" "fmt" "net/http" ) func Challenge(w http.ResponseWriter, r *http.Request) { var d struct { Type string `json:"type"` Token string `json:"token"` Challenge string `json:"challenge"` } if err := json.NewDecoder(r.Body).Decode(&d); err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Fprint(w, "Hello World!") return } if d.Type == "url_verification" { w.WriteHeader(http.StatusOK) fmt.Fprint(w, "{'challenge': %s}", d.Challenge) return } w.WriteHeader(http.StatusOK) fmt.Fprint(w, "OK") }
これを local から deploy するためのスクリプトを用意した。
#!/bin/sh while getopts f:p: OPT do case $OPT in "f" ) FUNCTION="$OPTARG" ;; "p" ) ENDPOINT="$OPTARG" ;; esac done if [ -z $FUNCTION ] ; then echo "function 名が指定されていません" exit 1; fi if [ -z $ENDPOINT ] ; then echo "endpoint が指定されていません" exit 1; fi gcloud functions deploy $FUNCTION --entry-point $ENDPOINT --trigger-http --runtime=go111 --region=asia-northeast1 --env-vars-file .env.yaml
deploy 実行するときは以下
$ scripts/deploy.sh -f function -p Challenge
現時点で想定しているディレクトリ構成はこんな感じになる。
. ├── .env.yaml ├── .gcloudignore ├── .gitignore ├── Makefile ├── go.mod ├── function.go └── scripts └── deploy.sh
.gcloudignore / .gitignore はよくある内容だし、 Makefile は 改訂2版 みんなのGo言語 に載ってるMakefileと同じような中身なので省略する。go.mod もファイルこそあるが、今回のサンプルで使うようなライブラリは特にない。このタイミングでは .env.yaml は空ファイルで良い。
上記 Qiita の Slack App の作成 は同じ手順になるので、ここでは記載しない。記事通りに作ると良い。
この時点でbotは返信がまだできないが、 Slack から Endpoint が叩かれればOK。
メンションを受け取れる状態にする
package sample import ( "encoding/json" - "fmt" + "log" "net/http" ) +type requestBody struct { + Type string `json:"type"` + Token string `json:"token"` + Challenge string `json:"challenge"` + Event eventData +} + +type eventData struct { + Type string `json:"type"` + UserID string `json:"user"` + Text string `json:"text"` + Timestamp string `json:"ts"` + ChannelID string `json:"channel"` + EventTimestamp string `json:"event_ts"` +} + func Challenge(w http.ResponseWriter, r *http.Request) { - var d struct { - Type string `json:"type"` - Token string `json:"token"` - Challenge string `json:"challenge"` - } + var d requestBody if err := json.NewDecoder(r.Body).Decode(&d); err != nil { w.WriteHeader(http.StatusInternalServerError) - fmt.Fprint(w, "Hello World!") + log.Fatalf("failed to parse: %v", r.Body) return } if d.Type == "url_verification" { w.WriteHeader(http.StatusOK) - fmt.Fprint(w, "{'challenge': %s}", d.Challenge) + log.Printf("succeeded to challenge: %s", d.Challenge) + return + } + + if d.Event.Type == "app_mention" { + str, err := json.Marshal(d.Event) + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + log.Fatalf("failed to parse: %v", r.Body) + } + w.WriteHeader(http.StatusOK) + log.Printf("your message is: %s", str) return } - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, "OK") + w.WriteHeader(http.StatusBadRequest) +}
このタイミングで slack から飛んでくるリクエストボディとその中に含まれるイベントを構造体で定義しておきパースするようにした。 app_mention
で何を受け取ったかはログに出せるように修正した。
ここで deploy すると以下のような感じになる。
メンションに返信できるようにする
slack bot からのメッセージは簡単のため incoming webhook で設定した。
https://api.slack.com/apps/{さっき作ったアプリの設定ページ} に飛ぶと一番下の方に
こういうのがあるので、その一番下の「Add New Webhook to Workspace」から新しい incoming webhook のURLを作る。
そこで作ったURLを .env.yaml に記載する。
SLACK_INCOMING_WEBHOOK: https://hooks.slack.com/services/your/webhook
さらに、この webhook を使って slack にメッセージを post する処理を作成する。 ( slack.go というファイル名とする。)
package jimiko import ( "encoding/json" "net/http" "os" "strings" ) func reply() (resp *http.Response, err error) { message := map[string]interface{}{ "text": "返信だよー", } jsonByte, err := json.Marshal(message) return postMessage(string(jsonByte)) } func postMessage(jsonStr string) (resp *http.Response, err error) { reader := strings.NewReader(jsonStr) resp, err = http.Post(os.Getenv("SLACK_INCOMING_WEBHOOK"), "application/json", reader) if err != nil { return nil, err } return resp, nil }
こうすることで .env.yaml に記載した slack の incoming webhook をハードコーディングしなくとも参照できるようになる。
次に slack.go で定義した関数を function.go から呼び出せるようにする。
} w.WriteHeader(http.StatusOK) log.Printf("your message is: %s", str) + _, err = reply() + if err != nil { + w.WriteHeader(http.StatusServiceUnavailable) + log.Fatalf("failed to post message to slack: %v", err) + } return }
今のディレクトリ構成はこうなる。
. ├── .env.yaml ├── .gcloudignore ├── .gitignore ├── Makefile ├── go.mod ├── jimiko.go ├── mention.txt ├── scripts │ └── deploy.sh └── slack.go
ここまでで deploy すると以下のような感じになる。
ここまでやっておくと、あとは飛んできたメンションの内容を見て返信する内容を変えたりとか好きにカスタマイズできる。
ここからのカスタマイズはまた気が向いたら記事にする。