Golang + lambda + lineBot(serverless)

# ついにGOがAWSにきた！！

🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉
ついにGolangがAws lambdaに対応しました！！！！
🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉

今まで、様々なライブラリを使ってなんとか無理くり、やっていましたが、これでできるようになりました！
それに伴い、serverlessも対応しましたので、早速バイト先のserverlessのボットのプロジェクトをしれっと全部Goにしたので、
その時のことを書いていきます。

## serverlessがどうなったのか

[serverless](https://serverless.com/)フレームワークは今は、使われているところが徐々に増えてきていますが、serverlessはこれまで、Goは対応しておらず、バイナリをaws cliであげるとか、pythonのCのライブラリとしてあげるなど、様々な方法でアプローチしていました。しかし、今回serverlessの公式がサポートした！とういう流れですね

## とりあえずserverless + go

まぁともかくやってみましょう

```shell
$ sls -v
1.26.0
$ sls create -t aws-go -p myservice
$ cd myservice
$ tree
.
├── Makefile
├── hello
│   └── main.go
├── serverless.yml
└── world
    └── main.go
```
こんな感じですね

とりあえずこいつらをデプロイしてみましょう！！！
makeファイルもgoであるがゆえに用意させたので、これを使いましょう！
（僕が最初にやったときは、なかった。。）
```
$ make build
$ sls deploy
$ sls invoke -f hello
{
    "message": "Go Serverless v1.0! Your function executed successfully!"
}
$ sls invoke -f world
{
    "message": "Okay so your other function also executed successfully!"
}
```

とりあえずこれでlambda関数はawsにあがりました！！バンザーーーイ！！！！

## おうむ返し

さて、ボットといえば**おうむ返し**です。
なので、今回はおうむ返しをやっていきます！！

```shell

$ tree
.
├── Makefile
├── bin
│   └── bot
├── bot
│   ├── line.go
│   ├── main.go
│   └── parse.go
├── conf
│   └── dev
│       └── dev.yml
└── serverless.yml
```

構成としてはこんな感じです
ではまず一つ一つ見ていきましょう

### 設定
 
serverless.yml

```yaml
service: myservice

provider:
  name: aws
  runtime: go1.x
  region: ap-northeast-1
  stage: ${opt:stage, self:custom.defaultStage}

custom:
  defaultStage: dev
  profiles:
    dev: sls
  otherfile:
    environment:
      dev: ${file(./conf/dev/dev.yml)}

package:
 exclude:
   - ./**
 include:
   - ./bin/**

functions:
  bot:
    handler: bin/bot
    environment:
      CHANNEL_SECRET: ${self:custom.otherfile.environment.${self:provider.stage}.CHANNEL_SECRET}
      CHANNEL_TOKEN: ${self:custom.otherfile.environment.${self:provider.stage}.CHANNEL_TOKEN}

    events:
      - http:
          path: callback
          method: post
          cors: true

```
./conf/dev/dev.yml

```yaml
CHANNEL_SECRET: "<your CHANNEL_SECRET>"
CHANNEL_TOKEN: "<your CHANNEL_TOKEN>"
```

基本的にここに、AWSのlambda以外のサービスをいくつ立てて接続してとかはここに記述します。
IAMもここで定義しますね。ですが今回はおうむ返しなので、特に設定することはないですが、lineの*CHANNEL_SECRET*と*CHANNEL_TOKEN*を環境変数として設定したいので、./conf/dev/dev.ymlに置いています。



次にコードを見ていきましょう。

### ソースコード

main.go

```go
package main

import (
	"errors"
	"fmt"
	"os"

	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
	"github.com/line/line-bot-sdk-go/linebot"
)

func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	line := Line{}
	err := line.New(
		os.Getenv("CHANNEL_SECRET"),
		os.Getenv("CHANNEL_TOKEN"))
	if err != nil {
		fmt.Println(err)
	}
	eve, err := ParseRequest(line.ChannelSecret, request)
	if err != nil {
		status := 200
		if err == linebot.ErrInvalidSignature {
			status = 400
		} else {
			status = 500
		}
		return events.APIGatewayProxyResponse{StatusCode: status}, errors.New("Bat Request")
	}
	line.EventRouter(eve)
	return events.APIGatewayProxyResponse{Body: request.Body, StatusCode: 200}, nil
}

func main() {
	lambda.Start(Handler)
}
```
ここでは先ほどの*CHANNEL_SECRET*, *CHANNEL_TOKEN*設定して、botのクライアントを作成しますが、一つ問題で、lineの公式のsdkの中にある、サンプルは、goのnet/httpを使っていますが、残念なことに、互換性がないので、自分で、構造体を定義してやるしかなかったので、その辺りはSDKの中で必要な部分をパクってきて、パーサーを書いています。

parse.go

```go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"

	"github.com/aws/aws-lambda-go/events"
	"github.com/line/line-bot-sdk-go/linebot"
)

func ParseRequest(channelSecret string, r events.APIGatewayProxyRequest) ([]*linebot.Event, error) {
	if !validateSignature(channelSecret, r.Headers["X-Line-Signature"], []byte(r.Body)) {
		return nil, linebot.ErrInvalidSignature
	}
	request := &struct {
		Events []*linebot.Event `json:"events"`
	}{}
	if err := json.Unmarshal([]byte(r.Body), request); err != nil {
		return nil, err
	}
	return request.Events, nil
}

func validateSignature(channelSecret, signature string, body []byte) bool {
	decoded, err := base64.StdEncoding.DecodeString(signature)
	if err != nil {
		return false
	}
	hash := hmac.New(sha256.New, []byte(channelSecret))
	hash.Write(body)
	return hmac.Equal(decoded, hash.Sum(nil))
}
```

ここでパースしています。詳しくはSDKの中を見るとわかります。

line.go

```go
package main

import (
	"fmt"

	"github.com/line/line-bot-sdk-go/linebot"
)

type Line struct {
	ChannelSecret string
	ChannelToken  string
	Bot           *linebot.Client
}

func (r *Line) SendTextMessage(message string, replyToken string) error {
	return r.Reply(replyToken, linebot.NewTextMessage(message))
}

func (r *Line) SendTemplateMessage(replyToken, altText string, template linebot.Template) error {
	return r.Reply(replyToken, linebot.NewTemplateMessage(altText, template))
}

func (r *Line) Reply(replyToken string, message linebot.Message) error {
	if _, err := r.Bot.ReplyMessage(replyToken, message).Do(); err != nil {
		fmt.Printf("Reply Error: %v", err)
		return err
	}
	return nil
}

func (r *Line) NewCarouselColumn(thumbnailImageURL, title, text string, actions ...linebot.TemplateAction) *linebot.CarouselColumn {
	return &linebot.CarouselColumn{
		ThumbnailImageURL: thumbnailImageURL,
		Title:             title,
		Text:              text,
		Actions:           actions,
	}
}

func (r *Line) NewCarouselTemplate(columns ...*linebot.CarouselColumn) *linebot.CarouselTemplate {
	return &linebot.CarouselTemplate{
		Columns: columns,
	}
}

func (l *Line) New(secret, token string) error {
	l.ChannelSecret = secret
	l.ChannelToken = token

	bot, err := linebot.New(
		l.ChannelSecret,
		l.ChannelToken,
	)
	if err != nil {
		return err
	}

	l.Bot = bot
	return nil
}

func (r *Line) EventRouter(eve []*linebot.Event) {
	for _, event := range eve {
		switch event.Type {
		case linebot.EventTypeMessage:
			switch message := event.Message.(type) {
			case *linebot.TextMessage:
				r.handleText(message, event.ReplyToken, event.Source.UserID)
			}
		}
	}
}

func (r *Line) handleText(message *linebot.TextMessage, replyToken, userID string) {
	r.SendTextMessage(message.Text, replyToken)
}

```

ここがイベントハンドリングや先ほどmainで出てきた自作の構造体が定義されているところです。
*EventRouter*でイベントの種類を分けて、handleText内でどうゆう風なものがきたらこうするとかの条件分岐を書くようにしています。
が今回はおうむ返しなので、そのまま返すようにしています。

### その他

はじめにslsでプロジェクトを作った時と、構造を変えたので、そのあた理を考慮してmakeファイルも変更しておきます。

```make
build:
	go get github.com/aws/aws-lambda-go/lambda
	env GOOS=linux go build -ldflags="-s -w" -o bin/bot bot/*.go
```

##デプロイ

デプロイは最初のものと同じ方法でできます。

```shell
$ make build
$ sls deploy
```

これであとは、出力されたurlをWebhook URLに設定してやればOK

![Screenshot_20180210-134939.png](https://qiita-image-store.s3.amazonaws.com/0/115970/68dff463-ea05-d2fe-6894-8208a2111465.png)


てな感じでseverlessを使ってline bot開発でした！！


### 参考

https://github.com/serverless/serverless/pull/4669

https://github.com/line/line-bot-sdk-go


単純にapiを作る場合は以下のリポジトリがいい感じですね

https://github.com/akrylysov/algnhsa