GolangとGoogle App Engineを使ってLINEBotを作ってみる

今やLINE BotやSlack Botなどのチャットボットを開発するのは、エンジニアの嗜みといっても過言ではないくらい当たり前のものになってきました。一方で筆者は恥ずかしながら何だかんだ今までLINE Botを作ったことがなく、先日が初実装でした。
しかし、筆者の構成で作っている最近の記事があまり多くなく動かすまでに予想以上に苦戦しました。ただ苦戦しただけだと辛いだけなので、将来の筆者へのメモとして、また同じような思いをする人が１人でも減ることを願って作成手順を記事化してみました。

## つくるもの
LINE Messaging APIを利用して、ありがちなオウム返しbotを

- WSL (Ubuntu 16.04 LTS)
- Go 1.11
- Google App Engine  

を使用して実装します。
LINE Messaging APIの構成などについては、[公式ドキュメント](https://developers.line.biz/ja/docs/messaging-api/overview/)をご覧ください。

## つくる準備
### LINE Messaging APIを利用可能な状態にする
まず、LINEBotを利用するために、LINE Developer Consoleにログインし、プロバイダとチャネルを作成します。チャネルを作成する際は、「Messaging APIでチャネル作成する」を選択してください。
このあたりの手順は公式の[Messaging APIを利用するには](https://developers.line.biz/ja/docs/messaging-api/getting-started/)に詳しく書かれています。

チャネルを作成した後は、チャネルにLINE botとして利用するための設定を行なっていきます。まずは以下の画像のように、アプリ名・説明・業種・メールアドレスの欄を埋めていきます。
![img2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320807/d32eaa5f-68a7-c0af-4705-d63853321474.png)

それらを埋めた後は、`Webhook送信`の欄を`利用する`に変更し、`自動応答メッセージ`を`応答しない`に、`友だち追加時あいさつ`を`利用しない`に変更します。
また、作ったBotを友達とのグループトークなどに投入したい場合は`Botのグループトーク参加`を`利用する`に変更しておきます。
![img4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320807/1648da02-c667-a128-5e88-3a50e93f8f61.png)

あとは、`Webhook URL`欄に後述するBotのデプロイ先URLを反映させれば、LINE Developer Console側での設定は完了です。
### GAEのセットアップをする
手順としては

1. Google Cloud SDKのインストールとセットアップ
2. Google App Engineアプリケーションの作成

となります。
1.については、[公式のインストールガイド](https://cloud.google.com/sdk/docs/?hl=ja#deb)に従ってインストールを行った後、

```bash
$ gcloud components install app-engine-go
```
でGolang用のコンポーネントをインストールすれば完了です。

2.については、[公式のApp Engine アプリケーション](https://cloud.google.com/appengine/docs/standard/go/console/?hl=ja#create)の`プロジェクトとアプリケーションを作成する`を参照すれば完了です。
自分の場合は、WebUI上でプロジェクトを作成した後

```bash
$ gcloud app create --project=<PROJECT_ID>
```

でアプリケーションを作成しました。

## つくってみる
LINE BotのアプリをGolangで作成するにあたり、SDKを使用するので、`go get`を行なって利用できる状態にします。

```bash
$ go get github.com/line/line-bot-sdk-go/linebot
```

### Bot本体のコードを記述する
このコードは基本的にはline-bot-sdk-goに含まれているEchoBotの[Examples](https://github.com/line/line-bot-sdk-go/blob/master/examples/echo_bot_handler/server_appengine.go)を元に記述しています。しかしこのExamplesが書かれたおよそ三年前から現在までの間にGolangやGAEの仕様変更が行われているため、最終的には以下のようなコードに書き換えます。

```go:app.go
package main

import (
	"fmt"
	"log"
	"net/http"
	"os"

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

func main() {
	// HTTP Handlerの初期化
	handler, err := httphandler.New(
		os.Getenv("CHANNEL_SECRET"),
		os.Getenv("CHANNEL_TOKEN"),
	)
	if err != nil {
		log.Fatal(err)
	}

	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
		log.Printf("Defaulting to port %s", port)
	}

	// 実際にRequestを受け取った時に処理を行うHandle関数を定義し、handlerに登録
	handler.HandleEvents(func(events []*linebot.Event, r *http.Request) {
		bot, err := handler.NewClient()
		if err != nil {
			log.Print(err)
			return
		}

		for _, event := range events {
			if event.Type != linebot.EventTypeMessage {
				return
			}

			switch message := event.Message.(type) {
			case *linebot.TextMessage:
				replyText := message.Text
				if _, err = bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage(replyText)).Do(); err != nil {
					log.Print(err)
				}
			}
		}
	})

	// /callback にエンドポイントの定義
	http.Handle("/callback", handler)
	// HTTPサーバの起動
	log.Printf("Listening on port %s", port)
	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil))
}
```
上記コードについて簡単に解説していきます。
LINE Botアプリケーションとして必要な実装は、

1. (LINE Bot SDKに含まれている)httpHandlerの初期化
2. Handler関数の定義と登録
3. HTTPリクエストを処理するエンドポイントの定義
4. HTTPサーバの起動

となります。 

1.の初期化に関しては`handler, err := httphandler.New()`が
2.の定義と登録に関しては`handler.HandleEvents(func(events []*linebot.Event, r *http.Request) {})`が
3.の定義に関しては`http.Handle()`が
4. HTTPサーバの起動に関しては`http.ListenAndServe()`が

それぞれ対応します。

続いて、Handler関数`func(events []*linebot.Event, r *http.Request)`の中身について簡単に解説していきます。
LINEのサーバからWebhookで送信されるリクエストは、[Webhookイベントオブジェクト](https://developers.line.biz/ja/reference/messaging-api/#webhook-event-objects)に定義されている形式で送られてきます。このリクエストが`events`のSliceの中に格納されているため、そこからリクエストを取り出して逐次処理していきます。
上記のコードの場合、リクエストのイベントタイプ（上記のコードではメッセージイベントであるか）とメッセージイベントの形式（上記のコードではテキストであるか）を判別し、テキストメッセージのイベントであれば、メッセージの中身を`message.Text`で抜き出して、`bot.ReplyMessage().Do()`で返信を作成・送信しています。

ここまでで本体のコードは終わりです。

### GAEにデプロイするための設定ファイルを書く
今回はGAEにデプロイするために必須の`app.yaml`に加え、tokenなどを格納する`secret.yaml`を作成します。

```yaml:app.yaml

runtime: go111

includes:
- secret.yaml

handlers:
- url: /.*
  script: auto
  secure: always
  redirect_http_response_code: 301
```
```yaml:secret.yaml
env_variables:
  CHANNEL_SECRET: "<LINE Developer Consoleのチャネルシークレット>"
  CHANNEL_TOKEN: "<LINE Developer Consoleのチャネルアクセストークン>"
```
`app.yaml`の`handlers:`以下は、httpsでの通信を強制する（HTTPで通信した場合HTTPSにリダイレクトさせる）ためのオプションです[^1]。LINE Messaging APIではWebhook接続先にhttps実装を必須としているので、保険の意味も含めてこの設定を適用しています。
`secret.yaml` の利用はTOKENなど実行に必要だけど秘匿すべき情報を安全に記述するための方法です[^2]。そのため、`secret.yaml`は絶対に`.gitignore`に記述し、**公開しないように**してください。

### デプロイする
まずは、書いたgoのプログラムをビルドします。
※ 環境によっては`export GO111MODULE=on`をあらかじめ設定する必要があります。[^3]

```bash
$ go mod init <YOUR_PROGRAM_PATH>
$ go build
```

ビルドが完了したら、実際にGAEにデプロイします。

```bash
$ gcloud app deploy
$ gcloud app browse
```
`gcloud app browse`の実行後、デプロイ先URLが（おそらく`PROJECT_ID.appspot.com`だと思います）表示されるので、最後にLINE Developer ConsoleでWebhook URLに`PROJECT_ID.appspot.com/callback`と指定してください。

以上で作業は全て完了です！

## 完成
無事にデプロイできたら、作ったBotを友だち登録してみて実際に動作確認してみましょう
以下のような形で実行できるはずです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320807/971df112-20c8-6261-674a-8fa82c8aefa1.png" width="50%">

## 発展：グループからLINE Botを退出できるようにする
作成したLINE Botは、LINE Developer Console上で`Botのグループトーク参加`を`利用する`に変更すると、友だちとのグループトークなどにBotを投入して、使用することができます。
しかしながら投入したBotをグループトークなどから退出させる方法は、執筆時点ではBot自らがleave APIを叩いて抜け出すしかないらしいです[^4]。そのためBotがleave APIを叩けるようにあらかじめ実装しておかないと、Botを退出させたいのにできない…！なんて恐れが出てきます。

そこでここでは、実際にleave APIを叩く実装を追加します。
今回実装する仕様としては、

- ユーザから「leave」というテキストメッセージを発した場所がグループトークもしくはトークルームだった場合、Botは該当の会話から退出する
 - 上記に該当しない場所で発言された場合、オウム返しを含め何も行わない 

でいきます。

### Goのコードを追記する
まず、「leave」というメッセージが来た時だけ異なる動作をするために、元の`app.go`の`case *linebot.TextMessage:`の行の直後に、以下のコードを追記します。

```go:app.go
if message.Text == "leave" {
	leaveGroup(bot, event.Source)
	return
}
```
その後、実際にleave APIを叩く関数`leaveGroup()`を追記します。

```go:app.go
// グループ・トークルームから退出させる
func leaveGroup(bot *linebot.Client, eventSource *linebot.EventSource) {
	switch eventSource.Type {
	case linebot.EventSourceTypeGroup:
		if _, err := bot.LeaveGroup(eventSource.GroupID).Do(); err != nil {
			log.Print(err)
		}
	case linebot.EventSourceTypeRoom:
		if _, err := bot.LeaveRoom(eventSource.RoomID).Do(); err != nil {
			log.Print(err)
		}
	}
}
```

以上のコードを追記して再デプロイすることで、作ったBotをグループトークでも安心して使用することができます。

## 最後に
少し手順が多いようにも見えますが、自分は初めてのLINE Botをこのように実装しました。 
公式チュートリアルではHerokuを使用していましたが、上記のように実装することで、GAEでもLINE Botを動かすことができるので、Herokuを使ったことない方は、是非こちらの方法も検討してみてはいかがでしょうか。
また、LINEさんの出しているLINEBotの各言語のサンプルを拝見しましたが、個人的にはGolangがかなりシンプルにかけると感じたので、初めて書く方は是非Golangで書いてみるのも良いと思います。

## 参考文献
- https://github.com/line/line-bot-sdk-go
- https://developers.line.biz/ja/reference/messaging-api/


[^1]: [app.yaml リファレンス](https://cloud.google.com/appengine/docs/standard/go/config/appref?hl=ja#handlers_element)
[^2]: [GAE で秘匿したい環境変数を安全に設定する](https://qiita.com/nirasan/items/9527ffa7d2f2fac2933e)
[^3]: [Go Modules](https://qiita.com/propella/items/e49bccc88f3cc2407745)
[^4]: [LINEの新しいMessaging APIを試してみた](https://www.nowsprinting.com/entry/2016/10/02/043410)