LINE ClovaとkintoneをSDK使わず連携してみた

# はじめに
先週、cybozu developer networkにて、
[LINE Clovaとkintoneを使って在庫管理をしよう！](https://developer.cybozu.io/hc/ja/articles/360015747791)
という LINEのスマートスピーカー **Clova** と **kintone** を連携した記事を公開しました!

上の記事ではそれぞれのSDKを使ってかなりぱぱっと作ったのですが、
元々SDKなしで実装していたので、この記事ではSDKは使わずLambdaのcallbackで実装したものを紹介します！
(kintoneもSDKではなくrequestモジュールでやります)

# 作る連携シナリオ
流石にdevnet記事と同じ内容だと飽きちゃうので、今回は、

* Clovaに話しかけてkintoneのアプリ内を検索
* もし該当するレコードが1件あれば、そのレコードの内容を話す
* もし該当するレコードがなければ、「ないよ」と話す
* もし該当するレコードが複数あれば、「複数あるよ」と話す

って感じに作ってみました。超シンプルですね！(￣ー￣)

# kintoneの設定
アプリの設定についてです！
今回はアプリストアにある**「FAQ」**を利用します！

適当にサンプルデータを入れてください
![2018-09-19_18h25_32.png](https://qiita-image-store.s3.amazonaws.com/0/153051/f0c84e15-3093-6da1-4715-32355dba8a96.png)

そして、フィールドコードが初期値のままだと扱いづらいので、
それぞれ、

|フィールド名 |フィールドコード |
|---|---|
|質問|question|
|回答|answer|

とします。
あとは **APIトークン** (権限：取得のみ) を発行してkintone側の準備は完了です！

# LINE Clovaの設定
かなり設定数が多いのでちょっと大変ですが、細かいところは上のdevnetの記事をご覧ください！
→ devnetの記事は超丁寧に書いたつもりです (^o^)

大事なところとしては、

* **呼び出し名** を設定 (これは当然)
* **ExtensionサーバーのURL**
    * AWS API GatewayのURLを記述してください
* **対話モデル** (スピーカーの対話モデル)
    * スロット (インテントの設定より先にこちらをやると楽です！)
    * インテント

などがあります！

## スロット
今回作成したカスタムスロットは以下です。
![2018-09-19_15h44_12.png](https://qiita-image-store.s3.amazonaws.com/0/153051/cbf27515-b68a-b9e8-b8c9-f38f9d50372c.png)


とりあえずで3種類単語を用意して、すべて1つのスロット(SearchVal)に格納されるようにしています。

## インテント
カスタムインテントは以下です。
![2018-09-19_15h44_03.png](https://qiita-image-store.s3.amazonaws.com/0/153051/915ef3dd-dc3e-0d20-267e-fca58d6cbf07.png)


実際にClovaに向けて発言しそうなサンプルを記述しました。

# AWS の設定
中間サーバーとしてAWSのLambda (と API Gateway) を利用します。
こちらも上記のdevnet記事内で説明しているので細かい設定は割愛します。

## API Gatewayの設定
認証をオープンにする or CORS設定をする必要があるかと思います！
※ CORSについては [こちら] (https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-cors.html) に説明があります！

## Lambda関数の作成
プログラムは Node.js で記述します！
Nodeのバージョンは 8.10 としてください！
→ async/await を利用します

コードの詳細は次へ

# コード
Lambda関数にのせるNode.jsのコードです！
こちらのコードは[GitHub](https://github.com/RyBB/kintoneCustomize/tree/master/Clova)に置いてあります。

httpリクエストのためにrequest-promiseモジュールを利用したので、

* request
* request-promise

をnpmでインストールしてください m(_ _)m

``` javascript
const request = require('request-promise');

// kintoneから該当するレコードの件数を返す処理
const getkintone = val => {
  // kintone用パラメータ
  const DOMAIN = '<domain>'; // sample.cybozu.com
  const URL = 'https://' + DOMAIN + '/k/v1/records.json';
  const APP_ID = '<app ID>';
  const API_TOKEN = '<API Token>';
  const headers = { 'X-Cybozu-API-Token': API_TOKEN };

  // 発話した単語をkintoneの文字列複数行フィールドでLike検索
  const params = {
    app: APP_ID,
    query: 'question like "' + val + '"',
    totalCount: true,
  };
  const options = {
    url: URL,
    method: 'GET',
    headers: headers,
    'Content-Type': 'application/json',
    json: params,
  };

  let text;
  // 該当するレコードを取得
  return request(options)
    .then(resp => {
      switch (resp.records.length) {
        case 0:
          text = val + 'に該当するレコードはありませんでした。';
          break;
        case 1:
          text = resp.records[0].answer.value;
          break;
        default:
          text = val + 'に該当するレコードが' + resp.totalCount + '件ありました!';
          break;
      }
      return text;
    })
    .catch(err => {
      console.log(err);
    });
};

// Clovaに返す処理
const setClova = async (text, bool, callback) => {
  // Clovaに返すJSON
  const body = {
    version: '1.0',
    sessionAttributes: {},
    response: {
      outputSpeech: {
        type: 'SimpleSpeech',
        values: {
          type: 'PlainText',
          lang: 'ja',
          value: text,
        },
      },
      card: {},
      directives: [],
      shouldEndSession: bool,
    },
  };

  const res = {
    statusCode: 200,
    headers: {
      'Content-Type': 'application/json;charset=UTF-8',
    },
    body: JSON.stringify(body),
  };
  callback(null, res);
};

exports.handler = async (event, conetext, callback) => {
  const req = JSON.parse(event.body).request;
  let text, word, val;

  // ユーザのアクションによって処理を振り分ける
  switch (req.type) {
    case 'LaunchRequest':
      text = 'kintoneで何を検索したいですか？';
      break;
    case 'SessionEndedRequest':
      text = 'さようなら!';
      break;
    case 'IntentRequest':
      // スロットの中身を格納
      word = req.intent.slots;
      // キーワードがわからなかった場合
      if (!word) {
        text = 'すみません。その言葉はまだ理解できません。';
        break;
      }
      // スロット内のキーワードが認識できていれば
      val = word.SearchVal.value;
      text = await getkintone(val);
      setClova(text, true, callback);
      break;
    default:
      text = 'kintoneで調べたい単語を教えてください';
      break;
  }
  // 対話を続けるので false を返す
  setClova(text, false, callback);
};
```

# プチ解説
コードについてちょっとだけ解説します！

## kintone からレコード取得

**「発話されたキーワード(val) → kintone側の質問フィールド(question)をlike検索」**
するような処理をクエリに追加してレコードの取得をしています。

``` javascript
  const params = {
    app: APP_ID,
    query: 'question like "' + val + '"',
    totalCount: true,
  };
```

そして、取得したレコードが、

* 0 の場合 → 該当するレコードがなかった旨を返す
* 1 の場合 → そのレコード内の回答フィールド(answer)の値を返す
* 1以上の場合 → 複数該当するレコードがあった旨を返す

と分けて処理してみました！

``` javascript
  let text;
  // 該当するレコードを取得
  return request(options)
    .then(resp => {
      switch (resp.records.length) {
        case 0:
          text = val + 'に該当するレコードはありませんでした。';
          break;
        case 1:
          text = resp.records[0].answer.value;
          break;
        default:
          text = val + 'に該当するレコードが' + resp.totalCount + '件ありました!';
          break;
      }
      return text;
    })
    .catch(err => {
      console.log(err);
    });
```

※ エラー処理はかなーり雑です。。
※ async/await を使うためにプロミスを返すようにしています。

## Clovaに発話させる処理
ここの部分は、
[AWS LambdaでLINE Clovaのスキルを作成する](https://qiita.com/takanakahiko/items/8c816b52411f91d7c6b4)
を参考にさせていただきました！

## Lambdaのイベントハンドラー
Clovaが受け取ったユーザーの発話情報は、

```javascript
exports.handler = async (event, conetext, callback) => {
```

のeventの中に格納されています。そのため、このeventの中の情報を取得して
分岐処理をさせています。

# おわりに
ということで、
ClovaとkintoneをSDKなしで連携してみました！！

思ったより難しくはなかったですが、やはりSDKを使わないと設定部分の記述量が多くなりますね。。

それでは！≧(+･` ཀ･´)≦