4
2

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 1 year has passed since last update.

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

Last updated at Posted at 2020-12-02

#概要
4回目の投稿です。
以前、@yagi_engさんがスピーカーを務めたLINEBOTの勉強会に参加してきたので、復習がてら作成記事を書きます。
今回はGoで下記のように自分の位置情報を送信すると、周辺のホテル情報を返すLINEBOTを作成しました。
image.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

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

##楽天APIに登録する
楽天APIの利用にはアプリIDが必要になります。
Rakuten Developersより、アプリIDの発行を行うことができます。
こちらも利用には楽天会員のアカウントが必要になります。

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

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

#環境構築
DockerでGoの開発環境を構築する
こちらの記事を参考に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関数でローカルホストを立ち上げます。

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の手順に沿って実装していきます。
実装は下記のようになってます。

(hotelInfoBack()は記事後半で説明します。)

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

ngrokを使って、外部からローカルのサーバにアクセスするためのURLを発行します。(ngrokの使い方はこちら)
発行したhttps://......ngrok.io/callback入力し、updateをクリックすれば、登録は完了です。
image.png

起動

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

image.png

go runでローカルのサーバを立ち上げてから、メッセ―ジを送信すると定型文が返って来るようになりました....が余計なメッセージも返って来ています。
image.png

##自動応答機能をオフにする
上記の問題は、MassaginAPIタブのAuto-reply messagesの設定を変更することで解決できます。
画面右側のeditをクリックして、応答メッセージをオフにすれば、余計なメッセージが返ってくることはなくなります。
image.png

image.png

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

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に用意されているカルーセルテンプレートを使います。(詳しくはこちら)

ホテル情報を取得する

楽天APIの利用

ホテル情報の検索には楽天トラベル施設検索APIを利用します。
今回は下記のパラメータをリクエスト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テストフォームより、適当なjsonを取得し、json整形ツールに入れてから、JSON-to-Goを使うことでパースする構造体のコードを取得することができます。
image.png

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

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()の実装です。

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

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

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


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

#参考

4
2
0

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
4
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?