新卒がGoと楽天APIで宿探しBOT作ってみた

#概要
4回目の投稿です。
以前、[@yagi_eng](https://qiita.com/yagi_eng)さんがスピーカーを務めたLINEBOTの勉強会に参加してきたので、復習がてら作成記事を書きます。
今回はGoで下記のように自分の位置情報を送信すると、周辺のホテル情報を返すLINEBOTを作成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/817032/7ccf54c7-24fb-c4fd-8292-0874a0603a50.png)

機能としては大きく2つです。

- 位置情報以外の情報（画像やテキスト）を送信すると、「位置情報を送信してください。」と定型文を返す。
- 位置情報を送信すると、周辺のホテル情報を最大10件返す。

#各種ツール
- go 1.14
- VS Code 1.51.1
- Docker 19.03.13
- ngrok 2.3.35
- github.com/line/line-bot-sdk-go v7.6.0+incompatible
- 楽天トラベル施設検索API (version:2017-04-26)

# 構成
ローカルで立ちあげたサーバをngrokを使って外部公開し、LINF Platformに登録する形をとっています
周辺のホテル情報の検索には楽天トラベル施設検索APIを利用しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/817032/dc14d20e-2ad9-f996-1f4f-71dc56b10a57.png)


#ツールの準備をする
##LINE Developersに登録する
実装に入る前にまずは、LINEが提供するMassaging APIを利用するために下記の三つを行い、APIを利用するための`Channel secret`及び`Channel access token`を取得する必要があります。取得にはLINEのアカウントが必要になります。

- [LINE Developersに登録する](https://developers.line.biz/ja/docs/messaging-api/getting-started/#using-oa-manager)
- [チャネルを作成する](https://developers.line.biz/ja/docs/messaging-api/getting-started/#_4-%E3%83%81%E3%83%A3%E3%83%8D%E3%83%AB%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B)
- [ボットを作成する](https://developers.line.biz/ja/docs/messaging-api/building-bot/)

##楽天APIに登録する
楽天APIの利用には`アプリID`が必要になります。
[Rakuten Developers](https://webservice.rakuten.co.jp/)より、アプリIDの発行を行うことができます。
こちらも利用には楽天会員のアカウントが必要になります。

##line-bot-sdkを取得する
実装には``line-bod-sdk-go``を利用するため、go get します。

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


#環境構築
[DockerでGoの開発環境を構築する](https://qiita.com/uji_/items/8c9eda89526abe0ba900)
こちらの記事を参考にDockerでコンテナを作り、8080ポートを割り当ててます。

```
CONTAINER ID     IMAGE        COMMAND  CREATED      STATUS         PORTS                   NAMES
e56630e11723     linebot_app  "bash"   2 days ago   Up 2 seconds   0.0.0.0:8080->8080/tcp  linebot_app_1
```

# 定型文の応答
##ローカルホストの立ち上げ
まずはmain関数でローカルホストを立ち上げます。

``` go
func main() {
	http.HandleFunc("/callback", callback)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

```
これで``localhost:8080/callback``にアクセスすると、callback関数が実行されるようになります。

次にcallback関数の実装です。
[https://github.com/line/line-bot-sdk-go](https://github.com/line/line-bot-sdk-go)の手順に沿って実装していきます。
実装は下記のようになってます。

(`hotelInfoBack()`は記事後半で説明します。)
   
``` go
func callback(w http.ResponseWriter, r *http.Request) {
	//チャネル作成時に取得したChannel secret及びChannel access tokenを引数に渡す
	bot, err := linebot.New(config.SECRET, config.TOKEN)
	if err != nil {
		log.Fatal(err)
	}
	//http.Requestを*linebot.Eventにパースする。
	events, err := bot.ParseRequest(r)
	if err != nil {
		if err == linebot.ErrInvalidSignature {
			w.WriteHeader(400) //Bad Request
		} else {
			w.WriteHeader(500) //Internal Server Error
		}
		return
	}

	for _, event := range events {
		//リクエストのイベントがメッセージの受信かどうか
		if event.Type == linebot.EventTypeMessage {
			//受信したメッセージの種類による分岐
			switch event.Message.(type) {
			case *linebot.LocationMessage: //位置情報を受信した場合
				hotelInfoBack(bot, event)
			default: //位置情報以外を受信した場合
				_, err = bot.ReplyMessage(event.ReplyToken, linebot.NewTextMessage(backMsg)).Do()
				//bakcMsg = "位置情報を送信してください。"
				if err != nil {
					log.Print(err)
				}
			}
		}
	}
}
```

## WebhookURLの登録
定型文返答の実装まで終わったら、作成したチャネルのMessageing APIタブからWebhookURLの登録を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/817032/431eb2b6-02b8-5c09-9c50-58f043a924ec.png)

ngrokを使って、外部からローカルのサーバにアクセスするためのURLを発行します。(ngrokの使い方は[こちら](https://qiita.com/hsaitoo/items/a469b104cefd6aa0a729))
``発行したhttps://......ngrok.io/callback``入力し、updateをクリックすれば、登録は完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/817032/9ac5a987-1c50-f3a0-1569-76f7814993fa.png)


## 起動
作成したチャネルのMessageing APIタブのQRコードを読み取り、友達追加します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/817032/613ea0b0-8b5b-0f0b-7f1d-e57cfb8fd441.png)


`go run`でローカルのサーバを立ち上げてから、メッセ―ジを送信すると定型文が返って来るようになりました....が余計なメッセージも返って来ています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/817032/474bf75a-6d95-2ec8-e08a-8a2ea5312eee.png)

##自動応答機能をオフにする
上記の問題は、MassaginAPIタブの``Auto-reply messages``の設定を変更することで解決できます。
画面右側の``edit``をクリックして、応答メッセージをオフにすれば、余計なメッセージが返ってくることはなくなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/817032/c0274c6c-f57d-7f07-0394-ec0b9765462c.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/817032/2ff3638c-9716-c3ae-9eb8-6c11bf3fe5c8.png)

#ホテル情報の応答
##応答のための処理
`hotelInfoBack()`を実装していきます。

``` go
func hotelInfoBack(bot *linebot.Client, e *linebot.Event) {
	msg := e.Message.(*linebot.LocationMessage)

	//受信した位置情報から、緯度経度を取得する
	lat := strconv.FormatFloat(msg.Latitude, 'f', 2, 64)
	lng := strconv.FormatFloat(msg.Longitude, 'f', 2, 64)

	//ホテル情報を取得する
	replyMsg, couldGetInfo := getHotelInfo(lat, lng)
	//取得に失敗した場合は、定型文（エラーが発生しました。）を返す。
	if !couldGetInfo {
		_, err := bot.ReplyMessage(e.ReplyToken, linebot.NewTextMessage(errorMsg)).Do()
		if err != nil {
			log.Print(err)
		}
	}

	//応答するカルーセルテンプレートを作成する
	res := linebot.NewTemplateMessage(
		"ホテル一覧",
		linebot.NewCarouselTemplate(replyMsg...).WithImageOptions("rectangle", "cover"),
	)
	//応答を返す
	_, err := bot.ReplyMessage(e.ReplyToken, res).Do()
	if err != nil {
		log.Print(err)
	}
}
```
受信した位置情報に含まれる緯度経度を引数に`getHotelInfo()`を実行し、周辺のホテル情報を取得しています。
応答にはMessaging APIに用意されているカルーセルテンプレートを使います。(詳しくは[こちら](https://developers.line.biz/ja/reference/messaging-api/#carousel))


## ホテル情報を取得する
### 楽天APIの利用
ホテル情報の検索には[楽天トラベル施設検索API](https://webservice.rakuten.co.jp/api/simplehotelsearch/)を利用します。
今回は下記のパラメータをリクエストURLにセットして検索を行います。

```
https://app.rakuten.co.jp/services/api/Travel/SimpleHotelSearch/20170426?[parameter]=[value]…
```
|入力パラメータ名| |
| ---------------------------------------------- | ----------------- |
| format(レスポンス形式）                                | json              |
| latitude(緯度）                                   | 送信された位置情報の緯度 |
| longitude(経度）                                  | 送信された位置情報の経度 |
| serchRadius(検索範囲 : km max = 3)                 | 1                 |
| datumType(緯度経度の表示形式) | 1 : 世界測地系               |
| applicationID                                  | 発行したアプリID         |

尚、今回利用する出力パラメータは以下のようになります。

| 出力パラメータ名 |
| -------- |
| HotelSpecial(施設特色)|
|  HotelName (施設名称）      |
|  HotelThumbnailURL  (施設画像サムネイルURL)     |
|HotelInformationURL(施設情報ページURL) |





###パースする構造体の作成
今回は取得したjsonを構造体にparseしています。
[APIテストフォーム](https://webservice.rakuten.co.jp/explorer/api/Travel/SimpleHotelSearch/)より、適当なjsonを取得し、[json整形ツール](https://tools.m-bsys.com/development_tooles/json-beautifier.php)に入れてから、[JSON-to-Go](https://mholt.github.io/json-to-go/)を使うことでパースする構造体のコードを取得することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/817032/1d1a2003-1786-aebf-9502-c5a48ff6e97e.png)

利用する出力パラメータのみを残した下記の構造体を使います。

``` go
type response struct {
	Hotels []struct {
		Hotel []struct {
			HotelBasicInfo struct {
				HotelName           string `json:"hotelName"`
				HotelInformationURL string `json:"hotelInformationUrl"`
				HotelSpecial        string `json:"hotelSpecial"`
				HotelThumbnailURL   string `json:"hotelThumbnailUrl"`
			} `json:"hotelBasicInfo,omitempty"`
		} `json:"hotel"`
	} `json:"hotels"`
}
```

###ホテル情報取得の実装
`getHotelInfo()`の実装です。

```go
func getHotelInfo(lat, lng string) ([]*linebot.CarouselColumn, bool) {
	url := fmt.Sprintf(apiURL, lat, lng, config.API_ID)
	r, err := http.Get(url)
	if err != nil {
		return nil, false
	}
	defer r.Body.Close()
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		return nil, false
	}

	var res response
	//構造体にパースする
	if err = json.Unmarshal(body, &res); err != nil {
		return nil, false
	}

	var ccs []*linebot.CarouselColumn

	//カルーセルカラムの作成
	for index, hotel := range res.Hotels {
		if index == 10 {
			break
		}
		cc := linebot.NewCarouselColumn(
			hotel.Hotel[0].HotelBasicInfo.HotelThumbnailURL,
			cutOutCharacters(hotel.Hotel[0].HotelBasicInfo.HotelName, 40),
			cutOutCharacters(hotel.Hotel[0].HotelBasicInfo.HotelSpecial, 60),
			linebot.NewURIAction("楽天トラベルで開く", hotel.Hotel[0].HotelBasicInfo.HotelInformationURL),
		).WithImageOptions("#FFFFFF")
		ccs = append(ccs, cc)

	}
	return ccs, true
}


```

引数で受け取った緯度経度を使い、APIをたたいて取得したjsonを構造体にパースしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/817032/2bd5f723-41f8-2c78-86fa-c02fcaa97c7a.png)

構造体スライスを取得したら、それをもとにfor文を回して、カルーセルカラムを作っていきます。
応答で返すカルーセルテンプレートはカルーセルカラムの配列をフィールドとして持っています。（詳しくは[こちら](https://developers.line.biz/ja/reference/messaging-api/#column-object-for-carousel))
今回はカルーセルカラムにAPIから取得した、ホテル名、ホテルサムネイル画像、ホテル特色をセットしています。
また、`linebot.NewURIAction()`で楽天トラベルのホテルページへアクセスするactionを作成し、カルーセルカラムにセットしています。

尚、応答で返せるカルーセルカラムは最大10件となっています。
また、カルーセルカラムに設定する一部フィールドには文字数の制限があるため、文字を切り出す処理を書いています。


``` go

//cutOutCharacters 先頭から指定文字数だけを切りだす("abcde",3)　→　"abc"
func cutOutCharacters(s string, count int) string {
	if utf8.RuneCountInString(s) > count {
		return string([]rune(s)[:count])
	}
	return s
}
```
https://github.com/hiromu-saito/linebot/tree/master

#参考
- [DockerでGoの開発環境を構築する](https://qiita.com/uji_/items/8c9eda89526abe0ba900)
- [Go言語でJSONを扱う](https://qiita.com/nayuneko/items/2ec20ba69804e8bf7ca3)