LINE Clovaのスキル開発〜Node.jsで今日泊まれるホテルを検索！〜

# 概要
前回、下記の記事を書きましたが、続きとなります！
[LINE Clovaのスキル開発〜Node.jsでHelloWorld的な編〜](https://qiita.com/tsumasakky/items/1f8650c6a748b81370bb)

今回はClovaが指定の場所の今日泊まれるホテルを教えてくれるスキルを作ってみたので、ざっくりとご紹介します！
(これよく探すんだよな～終電逃したときとかに～(|||O⌓O;))

# 環境
Windows 8.1
Node.js 8.12.0
npm 6.4.1

# 1. スキル作成

Clovaのスキルを作成します。
Clova Developer Centerのセットアップがわからないよ～という方は前回記事をご覧ください ＼＼\(۶•̀ᴗ•́)۶//／／
[Clova Developer Centerのセットアップ](https://qiita.com/tsumasakky/items/1f8650c6a748b81370bb#clova-developer-center%E3%81%AE%E3%82%BB%E3%83%83%E3%83%88%E3%82%A2%E3%83%83%E3%83%97)

イメージは
自分「どこどこの今日泊まれるホテルを教えて～～」
Clova「こことこことこことここです」
自分「わーい 三└(┐卍^o^)卍ドゥルルル」
みたいなかんじです！

## 1-1. スロットの作成

前回は独自のカスタムスロットタイプを作成しましたが、今回は予め用意されているビルトインスロットタイプを使用します！

[ビルトインスロットタイプ](https://clova-developers.line.biz/guide/Design/Design_Guideline_For_Extension.md#BuiltinSlotType)
> ビルトインスロットタイプは、Clovaであらかじめ定義されている情報のタイプです。すべてのサービス(Extension)に共通で使用できる情報の表現が定義されています。ビルトインスロットタイプは、主に時間、場所、数量などのような情報を認識する必要がある場合に使用されます。上記の発話の場合、「2枚」に該当する情報を認識するためにビルトインスロットタイプを使用することができます。Clovaは、次のようなビルトインスロットタイプを提供しています。

早速、ビルトインスロットの「追加」ボタンから追加していきます。
![01_build_in_slot.png](https://qiita-image-store.s3.amazonaws.com/0/147118/f13a5ca0-986b-4c9d-12a9-a2a437d2fd9a.png)

ビルトインスロットのリストから
CLOVA.JP_ADDRESS_KEN
CLOVA.JP_ADDRESS_SHI
CLOVA.JP_ADDRESS_KU
にチェックし、「保存」します。
![02_build_in_slot_check.png](https://qiita-image-store.s3.amazonaws.com/0/147118/f92d95b9-2651-c514-6120-3a96e017ee40.png)

## 1-2. カスタムインテントを作成

では、設定したスロットを使ってインテントを作成していきます。
スロットリストに先ほどの3つのスロットを追加します。

|スロット名|スロットタイプ|
|:--|:--|
|Address_ken|CLOVA.JP_ADDRESS_KEN|
|Address_shi|CLOVA.JP_ADDRESS_SHI|
|Address_ku|CLOVA.JP_ADDRESS_KU|

![03_add_slot_to_list.png](https://qiita-image-store.s3.amazonaws.com/0/147118/2a9eef54-95d1-ed36-71eb-7f9bd52264c0.png)

そしてサンプル発話リストの設定です。

「"県"、"市"、"区"の今日泊まれるホテルを教えて」のように発話を設定し、
それぞれをカーソル選択してスロットを割り当てます。

ここまでできたらビルドします！！

# 2. APIの準備

## 2-1. Yahoo!ジオコーダAPI

所在地の緯度経度を取得するために、Yahoo!ジオコーダAPIを使いました！
[YOLP(地図):Yahoo!ジオコーダAPI - Yahoo!デベロッパーネットワーク](https://developer.yahoo.co.jp/webapi/map/openlocalplatform/v1/geocoder.html)

## 2-2. 楽天トラベル空室検索API
ホテルの情報は楽天のWeb APIを使わせていただきました！
[楽天ウェブサービス: 新規アプリ登録](https://webservice.rakuten.co.jp/app/create)
[楽天ウェブサービス: 楽天トラベル空室検索API(version:2017-04-26) | API一覧](https://webservice.rakuten.co.jp/api/vacanthotelsearch/)

それぞれ登録するとアプリケーションIDがもらえるので、後ほど使用しますヘ(°◇、°)ノ

# 3. バックエンドアプリの作成
アプリのセットアップは前回記事をご参考にしてください！
[2. バックエンドアプリの開発](https://qiita.com/tsumasakky/items/1f8650c6a748b81370bb#2-%E3%83%90%E3%83%83%E3%82%AF%E3%82%A8%E3%83%B3%E3%83%89%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E9%96%8B%E7%99%BA)

## 3-1. 追加npmパッケージのインストール

ターミナルでアプリのルートディレクトリに移動し、下記コマンドを実行します(ФДФ)

```terminal:terminal
$ npm i -s date-utils xmljson linq
```
date-utils: Node.jsで日付文字列のフォーマットを行うのに便利！
xmljson: xml⇔jsonの変換ができます！
linq: jsでLINQ式が使える！

## 3-2. 緯度経度検索モジュールの作成
ルートディレクトリに`models`というフォルダを作ります。
その中に`Geo.js`というファイルを作ってください。

ルートディレクトリに`services`というフォルダを作ります。
その中に`GeoService.js`というファイルを作ってください。

リクエストパラメータは下記の通りです！(かんたん)
[リクエストパラメータ一覧](https://developer.yahoo.co.jp/webapi/map/openlocalplatform/v1/geocoder.html#request-param)

|プロパティ|値|説明|
|:--|:--:|:--|
|appid(必須)|string|アプリケーションID。詳細は[こちら](https://www.yahoo-help.jp/app/answers/detail/p/537/a_id/43397)|
|query|string|住所文字列|

```js:Geo.js
module.exports = class Geo {
    constructor(coordinates) {
        this.longitude = coordinates.split(",")[0]
        this.latitude = coordinates.split(",")[1]
    }
}
```

```js:GeoService.js
const request = require("request");
const xml2json = require('xmljson').to_json
const Geo = require('../models/Geo')
require('date-utils')

module.exports = class GeoService {
  constructor(appId, location) {
    this.appId = appId
    this.location = location
  }

  async getCoordinates() {
    let appId = this.appId
    let location = this.location

    return new Promise( (resolve, reject) => {
      request({
        url: "https://map.yahooapis.jp/geocode/V1/geoCoder",
        qs: {
          appid: appId,
          query: location
        }
      }, function (error, res, body) {
        if (!error && res.statusCode == 200) {
          xml2json(body, function (error, data) {
            // 結果が0件だとkeyが消える = undefinedエラーで落ちる
            if('Feature' in data.YDF) {
              // 緯度経度オブジェクトをnewしたデータモデルオブジェクト(Geo.js)にマッピング
              resolve(new Geo(data.YDF.Feature[0].Geometry.Coordinates))
            }else{
              reject(error)
            }
          })
        } else {
          reject(error)
        }
      })
    })
  }
}
```


## 3-3. ホテル検索モジュールの作成

ルートディレクトリに`services`というフォルダを作ります。
その中に`HotelService.js`というファイルを生成してください！

リクエストパラメータは下記の通りです。
[入力パラメータ](https://webservice.rakuten.co.jp/api/vacanthotelsearch/#inputParameter)

|プロパティ|値|説明|
|:--|:--:|:--|
|applicationId|string|アプリケーションID。必須。|
|format|string|レスポンス形式。"json"or"xml"を選択可能|
|checkinDate|date|チェックイン年月日。"YYYY-MM-DD"|
|checkoutDate|date|チェックアウト年月日。"YYYY-MM-DD"|
|latitude|decimal|緯度。日本測地系 or 世界測地系(datumTypeによる)|
|longitude|decimal|経度。日本測地系 or 世界測地系(datumTypeによる)|
|datumType|int(1)|緯度経度タイプ。1 = 世界測地系、単位は度。2 = 日本測地系、単位は秒。|
|searchRadius|int|検索半径（km）0.1 <= x <= 3.0|

※ リクエストパラメータには区分コード、施設番号、緯度経度いずれかが指定されていることが必須です。
   複数指定された場合の優先順位は[施設番号＞緯度経度＞区分コード]となります。


```js:HotelService.js
const request = require("request");
const xml2json = require('xmljson').to_json
const Enumerable = require('linq')
require('date-utils');

module.exports = class HotelService {
  constructor(appId, coordinates) {
    this.appId = appId
    this.coordinates = coordinates
  }

  getTodaysHotels() {
    let appId = this.appId
    let coordinates = this.coordinates

    // 今日泊まれる -> チェックインが今日、チェックアウトが明日
    let today = HotelService.getDateFormarted(0)
    let tommorow = HotelService.getDateFormarted(1)

    return new Promise((resolve, reject) => {
      request.get({
        url: "https://app.rakuten.co.jp/services/api/Travel/VacantHotelSearch/20170426",
        qs: {
          applicationId: appId,
          format: "xml",
          checkinDate: today,
          checkoutDate: tommorow,
          latitude: coordinates.latitude,
          longitude: coordinates.longitude,
          datumType: 1,
          searchRadius: 1
        }
      }, function (error, res, body) {
        if (!error && res.statusCode == 200) {
          xml2json(body, (error, data) => {
            let hotels = data.root.hotels.hotel

            let hotelNames = []

            // 検索結果が1件しかない場合、Jsonの構成が変わる（謎）
            if ('hotelBasicInfo' in hotels) {
              hotelNames.push(hotels.hotelBasicInfo.hotelName)
              resolve(hotelNames)
              return
            }else{
              hotelNames = Enumerable.from(hotels)
                                     .select(x => x.value.hotelBasicInfo.hotelName)
                                     .toArray()
              resolve(hotelNames)
            }
          });
        } else {
          reject(error)
          return
        }
      })
    })
  }
  // パラメータに0を渡せば今日の日付を返してくれる
  // 1を渡せば明日の日付、2を渡せば明後日の日付...みたいな
  static getDateFormarted(day) {
    let dt = new Date()
    dt.setDate(dt.getDate() + day)
    return dt.toFormat("YYYY-MM-DD")
  }
}
```


そして、`app.js`です　ꉂ (๑¯ਊ¯)σ л̵ʱªʱªʱª

```js:app.js
const clova = require('@line/clova-cek-sdk-nodejs')
const express = require('express')

// アプリのID
const EXTENSIONID = process.env.EXTENSIONID
const RAKUTEN_WEB_API_APP_ID = process.env.RAKUTEN_WEB_API_APP_ID
const YAHOO_GEO_API_APP_ID = process.env.YAHOO_GEO_API_APP_ID

// Clova Developer Centerで設定したExtension IDを使ってリクエストの検証を行うことができる
const clovaMiddleware = clova.Middleware({
    applicationId: EXTENSIONID
})

const HotelService = require('./services/HotelService')
const GeoService = require('./services/GeoService')

// 発話設定
const clovaSkillHandler = clova.Client
    .configureSkill()

    // 起動時に喋る
    .onLaunchRequest(responseHelper => {
        responseHelper.setSimpleSpeech({
            lang: 'ja',
            type: 'PlainText',
            value: '起動しました。',
        })
    })

    // ユーザーからの発話イベントが来たら反応
    .onIntentRequest(async responseHelper => {
        const intent = responseHelper.getIntentName()
        console.log('Intent : ' + intent)

        const sessionId = responseHelper.getSessionId()

        const slots = responseHelper.getSlots()
        console.log(slots);

        // デフォルトのスピーチ内容
        let speech = {
            lang: 'ja',
            type: 'PlainText',
            value: 'こんにちは'
        }

        let location = slots.Address_ken + slots.Address_shi + slots.Address_ku
        let coordinates = {}

        // 住所の緯度経度を検索
        try {
            let geoService = new GeoService(YAHOO_GEO_API_APP_ID, location)
            coordinates = await geoService.getCoordinates()
        // 見つからないときは見つけられませんでしたと白状するいい子
        } catch (e) {
            speech.value = location + " を見つけられませんでした。"
            return
        }

        // 緯度経度から空きのあるホテル名を検索
        try {
            let hotelService = new HotelService(RAKUTEN_WEB_API_APP_ID, coordinates)
            let hotelNames = await hotelService.getTodaysHotels()

            // ホテル名をカンマ区切りで返してるけど、なんでもいい
            speech.value = hotelNames.join(",")
        // 見つからないときは見つけられませんでしたと白状するいい子
        } catch (e) {
            speech.value = location + " 周辺の空室を見つけられませんでした。"
            return
        }

        responseHelper.setSimpleSpeech(speech)
        responseHelper.setSimpleSpeech(speech, true)
    })

    //終了時
    .onSessionEndedRequest(responseHelper => {
        const sessionId = responseHelper.getSessionId()
    })
    .handle();

const app = new express()
const port = process.env.PORT || 3000

app.post('/', clovaMiddleware, clovaSkillHandler)

app.listen(port, () => console.log(`Server running on ${port}`))
```
# 4. Herokuにデプロイする
これも前回の記事を参考にしてください！
[Herokuにデプロイする](https://qiita.com/tsumasakky/items/1f8650c6a748b81370bb#3-heroku%E3%81%AB%E3%83%87%E3%83%97%E3%83%AD%E3%82%A4%E3%81%99%E3%82%8B)

環境変数は下記の3つです。Herokuの「Config Vars」に設定してください。
・EXTENSIONID -> ClovaスキルのアプリケーションID
・RAKUTEN_WEB_API_APP_ID -> 楽天WebAPIのアプリケーションID
・YAHOO_GEO_API_APP_ID -> Yahoo!ジオコーダAPIのアプリケーションID

# 動作テスト

テストを行うのに、バックエンドの接続先を指定する必要があります。
前回の記事を参考にしてください！
[Clova Developer Centerからテストする](https://qiita.com/tsumasakky/items/1f8650c6a748b81370bb#4-clova-developer-center%E3%81%8B%E3%82%89%E3%83%86%E3%82%B9%E3%83%88%E3%81%99%E3%82%8B)

![05_speech_test.png](https://qiita-image-store.s3.amazonaws.com/0/147118/cc13b4ca-6632-8e3d-7085-4204c43598a6.png)

# まとめ
Clovaで今日泊まれるホテルを教えてくれるスキルをつくりました！ ≡┗( ^o^)┛≡┏( ^o^)┓≡┗( ^o^)┛ﾜｰｲ

# 感想
各APIがもっともっとやれることたくさんあるので、他にもいろいろできそう！！
あとビルトインスロットタイプめちゃくちゃ便利ですね！！