sam、lambda 、Go 、slackbot でメモ管理アプリを作る
概要
メモ管理アプリbot を作って、slack bot に入門します。
今回は、
lambda 、Go 、slackbot でタスク管理アプリ を ローカル環境 で利用できるまで
といった内容で作成します。 デプロイはしないのでお金は1円もかかりません。
全く実用的ではありませんが、勉強がてらに作成します。
きっかけ
slackBot の練習がしたかったため。
作るもの
メモリストの表示(メモ無し)
↓
メモの追加
↓
メモリストの表示(メモ有り)
↓
メモの削除(完了)
リポジトリ
前提
・Dockerインストール済
・aws cliインストール済
・sam cliインストール済
インストール手順は下記にメモしてます。
ローカルで実行できる lambda を作成
samで go テンプレートから lambda を作る
以下で雛形を作成します。
sam init --runtime go1.x --name aws-golang
enter で進んでいくと
下記のようなディレクトリ、ファイル群が作成されます。
├── Makefile
├── README.md
├── hello-world
│ ├── go.mod
│ ├── go.sum
│ ├── main.go
│ └── main_test.go
└── template.yaml
slack と アプリ の通信のための環境を用意
slack と ローカルで起動した アプリ間 の通信を行うために、
ngrok を使って アプリを 一時的に公開 してテストします。
下記の手順で公開できます。
- sam で ローカルで lamdaを起動
sam build
sam local start-api --env-vars env.json
- ngrok で公開
sam で起動した ローカルの 3000番ポートが使われるので、
ローカルの 3000 番を public にアクセスできるようにします。
ngrok http 3000
参考
slack アプリの作成 と URLの検証1
- slack 側 のアプリ の作成し、Token などを生成 します。
- こちら側で用意する メモ管理する アプリケーションの URL (ngrok で 公開時に 作成された一時URL) の疎通ができるか検証する必要があります。
この2点に関しては、下記記事でまとまっているので省略。
上記を参考に実装した main.go
を下記のように編集。
リクエストのjsonに格納されているchallengeというキーの値を、text形式でステータスコード200
で返す 必要があります。
package main
import (
"encoding/json"
"net/http"
"os"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/slack-go/slack"
"github.com/slack-go/slack/slackevents"
)
func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
// slack からのリクエストかどうかを検証する
err := Verify(ConvertHeaders(request.Headers), []byte(request.Body))
if err != nil {
return events.APIGatewayProxyResponse{Body: "slack conection error", StatusCode: 400}, err
}
body := request.Body
eventsAPIEvent, err := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionNoVerifyToken())
if err != nil {
return events.APIGatewayProxyResponse{Body: "slack conection error", StatusCode: 500}, err
}
// 受け取ったslack のイベントに応じて処理
switch eventsAPIEvent.Type {
case slackevents.URLVerification:
// slack に登録する の コールバック url(こちら側で処理するアプリ側のURL) が有効かどうか確かめるため
res, err := HandleURLVerification(body)
if err != nil {
return events.APIGatewayProxyResponse{Body: "slack conection error", StatusCode: 500}, err
}
return events.APIGatewayProxyResponse{
Body: res.Challenge,
StatusCode: 200,
}, nil
}
return events.APIGatewayProxyResponse{
Body: "",
StatusCode: 200,
}, nil
}
func ConvertHeaders(headers map[string]string) http.Header {
h := http.Header{}
for key, value := range headers {
h.Set(key, value)
}
return h
}
func HandleURLVerification(body string) (*slackevents.ChallengeResponse ,error) {
var res *slackevents.ChallengeResponse
if err := json.Unmarshal([]byte(body), &res); err != nil {
return nil, err
}
return res, nil
}
// slack からのリクエストかを検証 外部からのリクエストを受け付けないように
// ヘッダー、body、Signing Secretで検証
func Verify (header http.Header, body []byte) error {
verifier, err := slack.NewSecretsVerifier(header, os.Getenv("SLACK_SIGNING_SECRET"))
if err != nil {
return err
}
verifier.Write(body)
if err := verifier.Ensure(); err != nil {
return err
}
return nil
}
func main() {
lambda.Start(handler)
}
slack から メモを追加する
やること
メモの追加部分を実装します。
画面イメージ
実装
go.mod があるリポジトリで slack 操作用のパッケージをインストールします。
go の場合は下記のリポジトリがメジャーなようです。
go get github.com/slack-go/slack
先ほどの main.go を 下記のように編集します。
slackevents.ParseEvent
の戻り値でイベントを
slackevents.URLVerification
やslackevents.CallbackEvent
判定して各々処理します
func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
slackApi := slack.New(os.Getenv("SLACK_OAUTH_TOKEN"))
err := Verify(ConvertHeaders(request.Headers), []byte(request.Body))
if err != nil {
return events.APIGatewayProxyResponse{Body: "slack conection error", StatusCode: 400}, err
}
body := request.Body
eventsAPIEvent, err := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionNoVerifyToken())
if err != nil {
return events.APIGatewayProxyResponse{Body: "slack conection error", StatusCode: 500}, err
}
switch eventsAPIEvent.Type {
case slackevents.URLVerification:
res, err := HandleURLVerification(body)
if err != nil {
return events.APIGatewayProxyResponse{Body: "slack conection error", StatusCode: 500}, err
}
return events.APIGatewayProxyResponse{
Body: res.Challenge,
StatusCode: 200,
}, nil
// ↓ 追加したコード
case slackevents.CallbackEvent:
innerEvent := eventsAPIEvent.InnerEvent
switch event := innerEvent.Data.(type) {
case *slackevents.AppMentionEvent:
msg := strings.Split(event.Text, " ")
cmd := msg[1]
ctl := di.NewSlackMemoController()
switch cmd {
case "memo":
// 新しくメモを追加する ここは使用するDB に合わせて自由に変更してください
memo, err := ctl.CreateMemo(msg[2])
if err != nil {
return events.APIGatewayProxyResponse{Body: "bad request", StatusCode: 400}, err
}
responseMsg := fmt.Sprintf("%sを追加しました!", memo.Title)
_, _, err = slackApi.PostMessage(event.Channel, slack.MsgOptionText(responseMsg, false))
if err != nil {
return events.APIGatewayProxyResponse{Body: "bad request", StatusCode: 400}, err
}
}
}
// ↑ 追加したコード
}
return events.APIGatewayProxyResponse{
Body: "",
StatusCode: 200,
}, nil
}
slack で 保存されている全てのメモを取得
やること
DB から 保存されている メモのリストを 全て取得し
1件づつ、slack に ボタン式のメッセージを送信します。
今回は ボタン式の メッセージで メモの 完了
と 継続
を表現します。
下記の リポジトリ で 他にも色々な メッセージタイプ の実装例があるので参考にできます。
画面イメージ
実装
switch event := innerEvent.Data.(type) {
case *slackevents.AppMentionEvent:
msg := strings.Split(event.Text, " ")
cmd := msg[1]
ctl := di.NewSlackMemoController()
switch cmd {
case "memo":
memo, err := ctl.CreateMemo(msg[2])
if err != nil {
return events.APIGatewayProxyResponse{Body: "bad request", StatusCode: 400}, err
}
responseMsg := fmt.Sprintf("%sを追加しました!", memo.Title)
_, _, err = slackApi.PostMessage(event.Channel, slack.MsgOptionText(responseMsg, false))
if err != nil {
return events.APIGatewayProxyResponse{Body: "bad request", StatusCode: 400}, err
}
// ↓ 追加したコード
case "list":
// 保存されているメモのリストを通知する
// 全てのメモを取得 DB に合わせて実装してください
memos, err := ctl.GetMemos()
if err != nil {
return events.APIGatewayProxyResponse{Body: "bad request", StatusCode: 400}, err
}
if (len(memos) == 0) {
_, _, err = slackApi.PostMessage(event.Channel, slack.MsgOptionText("メモがありません。", false))
}
// slack へメモのリストを 1つづつ通知
for _, memo := range memos {
attachment := slack.Attachment{
Pretext: "pretext",
Fallback: "We don't currently support your client",
CallbackID: "accept_or_reject",
Color: "#3AA3E3",
Actions: []slack.AttachmentAction{
{
Name: "continue",
Text: "継続",
Type: "button",
Value: memo.ID,
},
{
Name: "complete",
Text: "完了",
Type: "button",
Value: memo.ID,
Style: "success",
},
},
}
message := slack.MsgOptionAttachments(attachment)
_, _, err = slackApi.PostMessage(event.Channel, slack.MsgOptionText("", false), message)
}
if err != nil {
return events.APIGatewayProxyResponse{Body: "bad request", StatusCode: 400}, err
}
}
// ↑ 追加したコード
}
slack で 保存されているメモをリストから削除する
やること
-
slack 側で メモの
完了
と継続
ボタンの内、
完了
を押した際に、対象のメモを DB から 削除します。 -
削除後、 slack の表示側では 削除対象のメッセージを
リストから削除しました!
と編集するようにします。
画面イメージ
実装
- slack アプリケーションに Interactive メッセージ用の エンドポイントを追加
slack からの ボタンのメッセージに 反応するエンドポイントを
slack アプリケーションに登録します。
Interactivity & Shortcuts
メニューの
Request URL に 登録したエンドポイントに slack からのリクエストが飛びます。
- slack から のリクエストを処理する lambda を作成
template.yaml に 新しく lambda 関数の 定義を追加します。
SlackInteractiveFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: slack-bot-memo-interactive/
Handler: slack-bot-memo-interactive
Runtime: go1.x
Architectures:
- x86_64
Events:
CatchAll:
Type: Api
Properties:
Path: /slack-bot/interactive
Method: POST
Environment:
Variables:
SLACK_OAUTH_TOKEN: !Ref SlackOauthToken
SLACK_SIGNING_SECRET: !Ref SlackSigingSecret
- リクエストを処理する lambda の実装
template.yaml
でCodeUri
にslack-bot-memo-interactive
をしたので指定した、
slack-bot-memo-interactive/
配下に 新たに main.go を 作成します。
application/x-www-form-urlencoded
形式でリクエストが飛んでくるので、
ローカルのアプリ(go)側 で エンコードして処理します。
リクエストの body の詳細な内容に関しては、
slack の公式か、下記の和訳記事があり、参考になります。
package main
import (
"encoding/json"
"fmt"
"net/url"
"os"
"github.com/aws/aws-lambda-go/events"
"github.com/aws/aws-lambda-go/lambda"
"github.com/slack-go/slack"
"github.com/tokatu4561/memo-app-use/pkg/application/di"
)
type Actions struct {
Name string `json:"name"`
Value string `json:"value"`
ActionType string `json:"type"`
}
type MessageActionPayload struct {
Actions []*Actions `json:"actions"`
CallbackId string `json:"callback_id"`
Channel struct {
Id string `json:"id"`
Name string `json:"name"`
} `json:"channel"`
User struct {
Id string `json:"id"`
Name string `json:"name"`
} `json:"User"`
ActionTs string `json:"action_ts"`
MessageTs string `json:"message_ts"`
AttachmentId string `json:"attachment_id"`
}
func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
slackApi := slack.New(os.Getenv("SLACK_OAUTH_TOKEN"))
// body を json にエンコード
q, _ := url.ParseQuery(request.Body)
qPayload := q.Get("payload")
// JSONを struct に読み取る
var payload MessageActionPayload
err := json.Unmarshal([]byte(qPayload), &payload)
if err != nil {
return events.APIGatewayProxyResponse{Body: "bad request", StatusCode: 400}, err
}
// continue 継続であれば削除処理 せずに レスポンス返す
if (payload.Actions[0].Name == "continue") {
return events.APIGatewayProxyResponse{Body: "", StatusCode: 200 }, nil
}
// 対象のメモを削除する 使用するDBに合わせて 実装してください。
ctl := di.NewSlackMemoController()
ctl.DeleteMemo(payload.Actions[0].Value)
// 削除したことをslackのチャンネルに通知する 対象のメッセージに対しを編集する
responseMsg := fmt.Sprintf("リストから削除しました!")
_, _, _, err = slackApi.UpdateMessage(payload.Channel.Id, payload.MessageTs, slack.MsgOptionText(responseMsg, false))
if err != nil {
return events.APIGatewayProxyResponse{Body: "bad request", StatusCode: 400}, err
}
return events.APIGatewayProxyResponse{
Body: "",
StatusCode: 200,
}, nil
}
func main() {
lambda.Start(handler)
}
詰まったところ
- lambda から dynamodb に接続できない
自分は ローカルで検証する用の db に dynamodb ローカル を使用していたのですが、
sam で起動する docker で動くlambda とdynamodDb が同じネットワーク内ある必要がありました。
docker network create
で docker 上に ネットワークを作成し、
dynamodbコンテナの ネットワークを指定
dynamodb:
image: amazon/dynamodb-local
command: -jar DynamoDBLocal.jar -sharedDb -dbPath . -optimizeDbBeforeStartup
volumes:
- dynamodb:/home/dynamodblocal
ports:
- 8000:8000
networks:
- lambda-local
local start-api
時に --docker-network
で 指定 する必要がありました。
sam local start-api --env-vars env.json --docker-network lambda-local
参考
- slack のリクエストを json で受け取れない
slack の interactive メッセージで アプリ側に リクエストを投げてくれるのですが
application/x-www-form-urlencoded
形式 のJSON文字列 でした。
application/json
形式だと思って、 普通に json を読み取ろうとすると、失敗します。
調べてみると
api gatway
で json に変換 して lambda に渡す ような方法が多く見つかりました。
sam template だと api gatway リソースに 下記 を追加すると良さそうでした。
x-amazon-apigateway-integration:
responses:
default:
statusCode: 200
httpMethod: POST
type: aws_proxy
contentHandling: CONVERT_TO_TEXT
requestTemplates:
application/x-www-form-urlencoded: "{\"body\": $util.urlDecode($input.json(\"$\"))}"
参考
しかし、sam で定義した リソースは基本的に lambda 関数しか検証できません。
今回は ローカルで試してみたく、
sam local start-api
で 起動した lambda では
api gatway
を通した 検証は 行えません。
記事で書いたように go 側で エンコード する対応としました。
感想
slackbotへの理解が深まって良かった
Discussion