AWS Lambda で動く Slack Bot を Go 言語で実装してみました。
目次
- 前提
- 今回作る Bot
- Slack Bot の作成
- Go のプロジェクト作成
- Makefile の編集
- serverless.yml の編集
- main.go の作成
- Slack の Event Subscriptions の設定
- 動作確認
- まとめ
- 参考資料
前提
- Serverless Framework が利用できるようになっていること
- Go がインストールされ、GOPATH の開発環境が整えられていること
- Go + Visual Studio Codeの開発環境を作る - Qiitaあたりを参考にしました
今回作る Bot
Slack Bot の作成
- https://api.slack.com/apps の「Create New App」より、アプリを作成します
- 「Building Apps for Slack」→「Add features and functionality」→「Bots」を選択します
- 「First, assign a scope to your bot token」→「Review Scopes to Add」より、Bot の権限を設定します
- ひとまず今回は、Bot へのメンションを読み取る権限と Channel への投稿権限を付与しておきます。
この時点で、一旦「Install App to Workspace」でワークスペースにアプリをインストールしておきます。
インストール後発行される「OAuth & Permissions」→「Tokens for Your Workspace」→「Bot User OAuth Access Token」は後ほど使うため、メモしておきます
- また、「Basic Information」→「App Credentials」→「Signing Secret」と「Verification Token」の値もメモしておきます
Go のプロジェクト作成
$ sls create -t aws-go-dep --name [Bot 名]
- デフォルトで作成されるファイル構成から以下のように変更します:
. |-- Gopkg.toml |-- Makefile |-- postMessageToSlack | `-- main.go `-- serverless.yml
Makefile の編集
- ビルド対象のファイル名が変更されたため、Makefile を以下のように変更します
.PHONY: build clean deploy build: dep ensure -v env GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o bin/postMessageToSlack postMessageToSlack/main.go # 複数のLambdaを用意する場合はここにビルド対象を追加していく clean: rm -rf ./bin ./vendor Gopkg.lock deploy: clean build sls deploy --verbose
serverless.yml の編集
serverless.yml
も編集します。
service: sample-bot frameworkVersion: '>=1.28.0 <2.0.0' provider: name: aws runtime: go1.x stage: dev region: us-east-2 package: exclude: - ./** include: - ./bin/** functions: postMessage: handler: bin/postMessageToSlack events: - http: path: invoke method: post environment: SLACK_ACCESS_TOKEN: [Slack App 作成時に発行された 「Bot User OAuth Access Token」] SLACK_VERIFICATION_TOKEN: [Slack App の Verification Token の値] SLACK_SIGNING_SECRET: [Slack App の Signing Secret の値]
main.go の作成
- Slack からの Event API を受け取って処理をするためのコードを書きます。いくつかのポイントに分けて説明します。
- 全ソースは下に記載しています
- Go 向けの Slack 用ライブラリ nlopes/slack を利用します。
ハンドラの定義
func HandleRequest(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error)
イベントのパース
- Slack から送られてきたイベントをパースします。
slackevents.ParseEvent
を用いると、リクエスト内容のパースと Token の検証が同時にできます。
body := request.Body eventsAPIEvent, err := slackevents.ParseEvent( json.RawMessage(body), slackevents.OptionVerifyToken( &slackevents.TokenComparator{ VerificationToken: os.Getenv("SLACK_VERIFICATION_TOKEN"), }, ), ) // パースもしくは Token の認証がうまく行かなかった場合のエラー処理 if err != nil { log.Printf("ERROR: %s", err) return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: http.StatusInternalServerError}, nil }
url_verification イベントの検証
- nlopes のリポジトリの例 を参考に以下のように実装します。
- Event API の type が「URLVerification」だった場合に、JSON に含まれる challenge キーの値をステータスコード 200 で返却します。
if eventsAPIEvent.Type == slackevents.URLVerification { var r *slackevents.ChallengeResponse err := json.Unmarshal([]byte(body), &r) if err != nil { return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: http.StatusInternalServerError}, nil } return events.APIGatewayProxyResponse{ Body: r.Challenge, Headers: map[string]string{ "Content-Type": "text"}, StatusCode: 200, }, nil }
各リクエストに対する検証
- Slack App では、上記の URL 認証の他に、より厳格な検証を実装することが推奨されています。
- 次の2つが一致するかを確認する処理を実装します
- nlopes では
slack.NewSecretsVerifier
メソッドを用いて検証することが可能です。
func convertHeaders(headers map[string]string) http.Header { header := http.Header{} for key, value := range headers { header.Set(key, value) } return header } func verifyRequest(request events.APIGatewayProxyRequest) error { body := request.Body headers := ConvertHeaders(request.Headers) sv, err := slack.NewSecretsVerifier(headers, os.Getenv("SLACK_SIGNING_SECRET")) if err != nil { return err } sv.Write([]byte(body)) return sv.Ensure() }
リクエストをハンドリングする
// CallbackEvent の場合 if eventsAPIEvent.Type == slackevents.CallbackEvent { innerEvent := eventsAPIEvent.InnerEvent switch event := innerEvent.Data.(type){ // Bot へのメンションの場合 case *slackevents.AppMentionEvent: // 正規表現を用いて、メッセージ内の「@Bot名 」を取り除く rep := regexp.MustCompile(`<@[A-Za-z0-9\.-_]*>\s`) text := rep.ReplaceAllString(event.Text, "") // 特定のチャンネルへのメッセージの投稿 api.PostMessage(event.Channel, slack.MsgOptionText(text, false)) } }
最終的な main.go
main.go
package main import ( "encoding/json" "log" "net/http" "os" "regexp" "github.com/aws/aws-lambda-go/events" "github.com/aws/aws-lambda-go/lambda" "github.com/nlopes/slack" "github.com/nlopes/slack/slackevents" ) // HandleRequest Lambda のリクエストをハンドルする func HandleRequest(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { body := request.Body log.Printf("Request Body: %s", body) eventsAPIEvent, err := slackevents.ParseEvent( json.RawMessage(body), slackevents.OptionVerifyToken( &slackevents.TokenComparator{ VerificationToken: os.Getenv("SLACK_VERIFICATION_TOKEN"), }, ), ) if err != nil { log.Printf("ERROR: %s", err) return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: http.StatusInternalServerError}, nil } log.Printf("EventType: %s", eventsAPIEvent.Type) if eventsAPIEvent.Type == slackevents.URLVerification { var r *slackevents.ChallengeResponse err := json.Unmarshal([]byte(body), &r) if err != nil { return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: http.StatusInternalServerError}, nil } return events.APIGatewayProxyResponse{ Body: r.Challenge, Headers: map[string]string{ "Content-Type": "text"}, StatusCode: 200, }, nil } if err := verifyRequest(request); err != nil { log.Fatalf("ERROR: %s", err) return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: http.StatusInternalServerError}, nil } if eventsAPIEvent.Type == slackevents.CallbackEvent { innerEvent := eventsAPIEvent.InnerEvent log.Printf("Inner Event: %s", innerEvent) switch event := innerEvent.Data.(type){ case *slackevents.AppMentionEvent: rep := regexp.MustCompile(`<@[A-Za-z0-9\.-_]*>\s`) text := rep.ReplaceAllString(event.Text, "") api := slack.New(os.Getenv("SLACK_ACCESS_TOKEN")) api.PostMessage(event.Channel, slack.MsgOptionText(text, false)) } } return events.APIGatewayProxyResponse{ Body: "OK", Headers: map[string]string{ "Content-Type": "text"}, StatusCode: 200, }, nil } func convertHeaders(headers map[string]string) http.Header { header := http.Header{} for key, value := range headers { header.Set(key, value) } return header } func verifyRequest(request events.APIGatewayProxyRequest) error { body := request.Body headers := convertHeaders(request.Headers) sv, err := slack.NewSecretsVerifier(headers, os.Getenv("SLACK_SIGNING_SECRET")) if err != nil { return err } sv.Write([]byte(body)) return sv.Ensure() } func main() { lambda.Start(HandleRequest) }
Slack の Event Subscriptions の設定
- ここまで完了したら、
make deploy
でデプロイします - 完了後表示されるメッセージ内の
endpoints:
タグの URL を Slack App に設定します- 設定する箇所は、「Event Subscriptions」から、「Enable Events」を On にした後の「Request URL」です
- URL 認証がうまくいくと、「Verified」と表示されます。
- 下の「Subscribe to bot events」→「Add Bot User Event」より、「app_mention」を追加します。
- これで、Slack App がワークスペース内のイベントを拾ってくれるようになります
動作確認
- 以下のように、Bot に対してメンションを送ると、オウム返ししてくれます
まとめ
- AWS Lambda で動く Slack Bot を Go で実装しました。
- nlopes/slack ライブラリのおかげで Slack の各種イベント等を扱うのが非常に楽になります。
- 少し複雑なイベントの処理なども挑戦してみようと思います。