画像を送ると何が写っているのかを返す LINE Bot を GCP で作った話

こんにちは @wezardnet です。
今年も Google I/O (2017) の時期がやってきました。残念ながら健康上の理由により参加を見送りましたが（チケット当選してないけど :sweat:）、また機会があれば参加してみたいです。

さて、今回は Google Cloud Vision API & Google Cloud Translation API を使って LINE で画像を送ると、その画像に何が写っているかを返す bot を作ってみました。プラットフォームは GAE/Go です :sunglasses:

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">LINE Messaging API + Google Cloud Vision API &amp; Google Translate API を使って写真を送ったら解析結果を返す bot を作ってみた。プラットフォームは GAE/Go でござる。 <a href="https://twitter.com/hashtag/gcpja?src=hash">#gcpja</a> <a href="https://twitter.com/hashtag/gcpug?src=hash">#gcpug</a> <a href="https://t.co/fAoU9yrUhM">pic.twitter.com/fAoU9yrUhM</a></p>&mdash; wezardnet (@wezardnet) <a href="https://twitter.com/wezardnet/status/862213770013057024">2017年5月10日</a></blockquote>

~~[QiitaでTwitterのツイートを埋め込める時代は終わりました](http://qiita.com/yumetodo/items/bb710bc890e6dd50b9e1) という事なのでサンプルアプリのスクショを貼り付けることにします ^^;~~


#1. LINE bot を作る準備

はじめに LINE bot を作るには [LINE BUSINESS CENTER](https://business.line.me/ja/) で登録した後 LINE Developers で Messaging API を使えるようにします。このあたりの事前準備は他にたくさん解説記事がありますので、今さら説明するまでもありません。特に以下の記事を読むと良いでしょう :point_up:

* [LINE Developer Trialを GAE/Go で始める #linedevday](http://qiita.com/koki_cheese/items/66980888d7e8755d01ec)
* [GAEにgolangでlinebotをデプロイする](http://qiita.com/fk_2000/items/f21cfa0c5ea0d61a9d20)


#2. 中身（コード）について

LINE bot には SDK が用意されているので、こちらを使うと Messaging API を直に使うより楽ちんです。今回はプラットフォームに GAE/Go を使うので Go 用の [line-bot-sdk-go](https://github.com/line/line-bot-sdk-go) を使います。ざっと見た感じ Go 以外には以下の言語の LINE bot SDK があるみたいです。

* [Python](https://github.com/line/line-bot-sdk-python)
* [Node.js](https://github.com/line/line-bot-sdk-nodejs)
* [Java](https://github.com/line/line-bot-sdk-java)
* [PHP](https://github.com/line/line-bot-sdk-php)
* [Perl](https://github.com/line/line-bot-sdk-perl)
* [Ruby](https://github.com/line/line-bot-sdk-ruby)

まずは必要なパッケージをインポートします。尚、コードは見づらくなるので一部省略してます。
ちなみにパッケージ管理は glide を使ってます。また Cloud Vision API と Cloud Translation API は独自のヘルパーパッケージにまとめてます。

```Go:Import
import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"

	"bitbucket.org/{my project}/lib/helper"

	"github.com/joho/godotenv"
	"github.com/line/line-bot-sdk-go/linebot"
	"github.com/zenazn/goji"

	"google.golang.org/appengine"
	"google.golang.org/appengine/log"
	"google.golang.org/appengine/urlfetch"
)
```

私はウェブフレームワークに [zenazn/goji](https://github.com/zenazn/goji) を使っているので、ルーティングは次のようになります。

```Go:init
func init() {
	m := goji.DefaultMux
	m.Post("/line/callback", lineCallbackHandler)
}
```

LINE bot 側のメッセージを受け取る（フックする） URL を LINE Developers の Channel 設定画面で登録してあげます。

![LINE developers.png](https://qiita-image-store.s3.amazonaws.com/0/12023/f6d8cfb5-5cf9-38e0-08c5-32f47b2936e3.png)

HTTP ハンドラ部分はおおむね [line-bot-sdk-go](https://github.com/line/line-bot-sdk-go) の [Example](https://github.com/line/line-bot-sdk-go/blob/master/examples/echo_bot_handler/server.go) を真似して実装すれば OK です。注意点としては GAE を使う場合、外部 API を叩く場合は urlfetch を使わなければならない点でしょうか…

```Go:lineCallbackHandler
func lineCallbackHandler(c web.C, w http.ResponseWriter, r *http.Request) {
	ctx := appengine.NewContext(r)

	// bot 用の設定ファイルを読み込む
	err := godotenv.Load("/line.env")
	if err != nil {
		log.Errorf(ctx, "LINE bot Setting file read error.: %v", err)
		return
	}

	bot, err := linebot.New(
		os.Getenv("LINE_BOT_CHANNEL_SECRET"), 
		os.Getenv("LINE_BOT_CHANNEL_TOKEN"), 
		linebot.WithHTTPClient(urlfetch.Client(ctx)), 
	)
	if err != nil {
		log.Errorf(ctx, "LINE bot new error.: %v", err)
		return
	}

	// リクエストを受信する
	events, err := bot.ParseRequest(r)
	if err != nil {
		if err == linebot.ErrInvalidSignature {
			w.WriteHeader(http.StatusBadRequest)
		} else {
			w.WriteHeader(http.StatusInternalServerError)
		}
		return
	}

	for _, event := range events {
		if event.Type == linebot.EventTypeMessage {
			var replyMsg string = ""
			switch message := event.Message.(type) {
				case *linebot.TextMessage:
					log.Infof(ctx, fmt.Sprintf("message.Text = %s", message.Text))
					replyMsg = fmt.Sprintf("「%s」じゃねーよ。画像送ってくれよぉぉー", message.Text)

				case *linebot.ImageMessage:
					// 送信された画像を取得する
					mc, err := bot.GetMessageContent(message.ID).Do()
					if err != nil {
						log.Errorf(ctx, "LINE bot get content error.: %v", err)
						w.WriteHeader(http.StatusInternalServerError)
						return
					}
					data, _ := ioutil.ReadAll(mc.Content)

					vision, err := helper.NewVision(ctx, r)
					if err != nil {
						log.Errorf(ctx, "fatal new vision error.: %v", err)
						w.WriteHeader(http.StatusInternalServerError)
						return
					}

					// Cloud Vision API で画像を解析させる
					ret, err := vision.AnnotateImage(data)
					if err != nil {
						log.Errorf(ctx, "fatal annotate image error.: %v", err)
						w.WriteHeader(http.StatusInternalServerError)
						return
					}

					translate, _ := helper.NewTranslate(ctx, r)

					responses := ret.Get("responses")
					for i := 0; i < len(responses.MustArray()); i++ {
						annotations := responses.GetIndex(i).Get("labelAnnotations")
						for j := 0; j < len(annotations.MustArray()); j++ {
							annotation := annotations.GetIndex(j)
							jdescription, _ := translate.TranslateEn2Ja(annotation.Get("description").MustString())
							replyMsg += "お前が送ってきた画像だけど、『" + jdescription + "』だろ。おれの画像認識力を甘く見ちゃダメだぜｗ"
							break
						}
						
						if replyMsg == "" {
							replyMsg += "なんだこれ？ (´・ω・｀)"
						}
						break
					}
			}
			if replyMsg != "" {
				if _, err = bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage(replyMsg)).Do(); err != nil {
					log.Errorf(ctx, "LINE bot Reply Message error.: %v", err)
				}
			}
		}
	}
}
```

Cloud Vision API で画像識別させた結果は複数の候補が返ってきますが、一番スコアが高いモノを採用して返すようにしてます。
また、テキストメッセージを受信した場合は、↓のような感じで画像を送ってもらうように催促しましょうｗ

![IMG_0702.png](https://qiita-image-store.s3.amazonaws.com/0/12023/9610b08e-3506-4ddc-d683-bfbdd29a8a27.png)


#3. あとがき

今回初めて LINE bot を作ったんですけど、サンプルが多いので意外と手軽に作れますね。実用的な bot を作るにはアイデア次第な気がします。ちなみに他にはどんな bot が作られているかは以下の記事を読むと良いでしょう。みなさん、おもしろい bot 作ってますね！

* [【LINE BOT】どんな bot が作られているか Qiita で調べてみた](http://qiita.com/minagoro0522/items/d4abd7add10a0da5e9c5)