15
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Golang + lambda + lineBot(serverless)

Posted at

ついにGOがAWSにきた!!

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

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

serverlessがどうなったのか

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

とりあえずserverless + go

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

$ 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にあがりました!!バンザーーーイ!!!!

おうむ返し

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


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

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

設定

serverless.yml

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

CHANNEL_SECRET: "<your CHANNEL_SECRET>"
CHANNEL_TOKEN: "<your CHANNEL_TOKEN>"

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

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

ソースコード

main.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

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

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ファイルも変更しておきます。

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

##デプロイ

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

$ make build
$ sls deploy

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

Screenshot_20180210-134939.png

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

参考

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

15
11
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?