Clova公式SDK(Node.js)の使い方まとめ

# はじめに
[Clova Extensions Kit(以下CEK)](https://clova-developers.line.me/)が公開され、[公式SDK](https://clova-developers.line.me/guide/#/CEK/Guides/Clova_CEK_SDK.md)もリリースされたものの、チュートリアルで使われてなかったり、ドキュメントも見当たらなかったり、サーバはご自由にという状況で戸惑った方も少なからずいらっしゃるのではないでしょうか（私がそうです）。
というわけでひとまず公式SDKのNode.js版ソースを流し読みしてみたので、使い方について簡単にまとめて説明してみようと思います。

※AWS(Lambda)、Firebase(Functions)での動かし方について↓の記事にまとめました。
[Clova公式SDK(Node.js)のAWS、Firebaseでの動かし方](https://qiita.com/miso_develop/items/bbf0f548d9d8c4ef2d5a)

※2018/8/20 アップデートによって追加されたメソッドの説明を追記しました。

* [setSessionAttributes](#setsessionattributessessionattributes)
* [getSessionAttributes](#getsessionattributes)
* [getUser](#getuser)

（`setSessionAttributes`、`getSessionAttributes`は、OSSで初プルリクしたものが無事マージされました…！）

# 公式SDK
2018/7/20時点では[Node.js版](https://github.com/line/clova-cek-sdk-nodejs)と[Swift版](https://github.com/line/clova-cek-sdk-swift)が公開されています。
今後も随時追加、更新されていくそうです。

今回はAlexaスキル、Google Assistantアプリの開発でもお馴染みの[Node.js版](https://github.com/line/clova-cek-sdk-nodejs)を説明していきます。

## インストール
`npm install`でさくっといけます。

```
$ npm i @line/clova-cek-sdk-nodejs
```

## 使い方

### require
書くまでもないですが一応。

```js
const clova = require('@line/clova-cek-sdk-nodejs');
```

### handler
`Client`クラスの`configureSkill`メソッドで`SkillConfigurator`クラスのインスタンスを生成してます。
そのあとに各リクエストイベントでの処理をonメソッドの引数に渡していきます。

```js
const handler = clova.Client
  .configureSkill()
  .onLaunchRequest(launchHandler)
  .onIntentRequest(intentHandler)
  .onSessionEndedRequest(sessionEndedHandler)
  .handle();
```

Clovaではリクエストが以下の4タイプあります。

|リクエスト|内容|
|---|---|
|LaunchRequest|スキル起動時のリクエスト。|
|IntentRequest|スキル起動～終了までの間呼ばれ続けるリクエスト。主にここをあれこれしてく。|
|SessionEndedRequest|スキル終了時のリクエスト。応答を返したりはできない。|
|EventRequest|ユーザの発話の有無に関わらず、クライアントの状態の変化や、それに伴うリクエストをExtensionに渡すために使用されるリクエスト。ハードウェアボタンとか？|

このうち`EventRequest`を除く3つのリクエストのイベントが用意されています。
引数のハンドラー関数にリクエストが呼ばれた際の処理を書いていきます。



### Contextクラス
各リクエストイベントから呼ばれるハンドラー関数の引数として`responseHelper`が渡されます。
この`responseHelper`は`Context`クラスのインスタンスで、インテント名やスロットを取得したりする色んな便利メソッドが詰まってるので簡単に説明していきます。

#### getSessionId()
セッションIDを取得します。

#### getIntentName()
インテント名を取得します。
`onIntentRequest`で拾ったリクエストは基本的にインテント名でswitchして処理を分岐させていきます。

#### getSlots()
スロットオブジェクトをごっそり取得します。

#### getSlot(slotName)
指定のスロット情報のみを取得します。

#### endSession()
このリクエストでスキルを終了させます。
Clovaから返事を返してそのままスキル終了させたいときはこのメソッドを入れときます。
スキルを継続したい場合はなにもしなくておっけー。

#### setSessionAttributes(sessionAttributes)
任意の情報をセッション変数に保持させます。
引数の`sessionAttributes`はオブジェクトです。
フラグ等、スキル実行中に継続して保持したい情報はこの`sessionAttributes`に格納しておきます。


また@h-takauma さんの「[Clova審査一発通過のための注意点](https://qiita.com/h-takauma/items/3147b5df2b50f3858a26#%E6%B3%A8%E6%84%8F%E7%82%B96%E3%82%BB%E3%83%83%E3%82%B7%E3%83%A7%E3%83%B3%E6%83%85%E5%A0%B1%E3%81%8C%E6%AE%8B%E3%81%A3%E3%81%9F%E3%81%BE%E3%81%BE%E3%81%AB%E3%81%AA%E3%81%A3%E3%81%A6%E3%81%84%E3%82%8B)」の記事でも書かれていますが、Clovaスキルは現在の仕様上、スキルを終了してもセッションが終了されません。
つまりスキル終了後すぐに再びスキルを呼び出すとセッション情報が残ったままスキルが実行されます。

プログラムの書き方によっては思わぬバグの原因にもなりますので、LaunchRequest時（スキル起動時）に明示的にクリアさせておくと良いでしょう。

```js
  ...
  
  .onLaunchRequest(responseHelper => {
    // sessionAttributesを明示的にクリア
    responseHelper.setSessionAttributes({})
    clova.SpeechBuilder.createSpeechText('スキル実行！')
  })
  
  ...
```

#### getSessionAttributes()
セッション変数を取得します。
オブジェクトがまるっと返ってきます。


#### getUser()
ユーザー情報を取得します。
返ってくるのはUser型オブジェクトです。

```js 
// User型
{
  "userId": "xxxxxxxx",
  "accessToken": "xxxxxxxx"
}
```

`accessToken`がない場合は`userId`のみ返ってきます。



### 返答メソッド
Clovaからの返答には主に以下の3タイプあり、それぞれのsetメソッドが用意されています。
`responseHelper`から同様に呼び出せます。

|返答タイプ|内容|
|---|---|
|SimpleSpeech|単文タイプの音声情報です。最も基本となるタイプです。|
|SpeechList|複文タイプの音声情報です。複数の文章を出力する際に使用されます。|
|SpeechSet|複合タイプの音声情報です。画面を持たないクライアントデバイスに、要約音声情報と詳細音声情報を渡す際に使用されます。|

また、それぞれのメソッドの最後の引数に`reprompt`をboolean型で指定でき(省略した場合は`false`)、`true`を指定した場合はユーザーが無反応の状態で一定時間経過後にClovaが聞き返しを行ってくれて、入力待ち時間を最大1回延長できます。
通常の返答とリプロンプトはそれぞれ個別にメソッドを実行しないとダメです。

```js
// まずこっちで返答して、
responseHelper.setSimpleSpeech(
  clova.SpeechBuilder.createSpeechText('通常の返答だよ！')
);

// ユーザーが無反応の状態で一定時間経過後にこっちを喋る
responseHelper.setSimpleSpeech(
  clova.SpeechBuilder.createSpeechText('リプロンプトだよ！'), true
);

// さらにユーザーが無反応の状態で一定時間経過するとスキルが終了される
```

#### setSimpleSpeech(speechInfo[, reprompt])
文章または音声データ(mp3とか)を返答させます。
引数の`speechInfo`は[SpeechInfoObject](https://clova-developers.line.me/guide/#/CEK/References/CEK_API.md#CustomExtSpeechInfoObject)という形式のオブジェクトになります。
なお後述する`SpeechBuilder`クラスに`value`の指定のみでSpeechInfoObjectオブジェクトを生成するメソッドがあります。

```js
// 文章
{
   "type": "PlainText",
   "lang": "ja",
   "value": "返答させる文章"
}

// 音声データ
{
  "type": "URL",
  "lang": "" ,
  "value": "https://www.example.com/sound.mp3"
}
```

#### setSpeechList(speechInfo[, reprompt])
複数の文章や音声データのURLを返答させます。
引数の`speechInfo`は配列になります。

#### setSpeechSet(speechInfoBrief, speechInfoVerbose[, reprompt])
画面があるデバイスとないデバイスで出力を出し分けるっぽいです。
多分動きとしては画面がある場合は`speechInfoBrief`で、ない場合は`speechInfoVerbose`で応答するみたいです。
現状画面なしデバイスしかなく、Clova Friendsで試した限りは`speechInfoVerbose`の応答のみ返ってきました。
対話モデル画面のテストからだと「応答がありません。(undefined)」となってしまいます。

`speechInfoBrief`はSpeechInfoObject形式のオブジェクトを渡します。
`speechInfoVerbose`はSpeechInfoObjectとは少し異なる形式のオブジェクトとなります。
SimpleSpeechとSpeechListのどちらかを以下の形式で渡します。

```js
// SimpleSpeech
{
  type: 'SimpleSpeech', 
  values: SpeechInfoObject
}

// SpeechList
{
  type: 'SpeechList', 
  values: Array<SpeechInfoObject>
}
```


### Clova.SpeechBuilderクラス
文章と音声データそれぞれの`SpeechInfoObject`オブジェクトを生成するスタティックメソッドがあります。

#### createSpeechText(value[, lang])
文章の`SpeechInfoObject`オブジェクトを生成してくれます。
また省略可能の第二引数で言語を指定できます。

```js
responseHelper.setSimpleSpeech(
  clova.SpeechBuilder.createSpeechText('文章だよ！')
)
```

#### createSpeechUrl(value[, lang])
音声データの`SpeechInfoObject`オブジェクトを生成してくれます。

```js
responseHelper.setSimpleSpeech(
  clova.SpeechBuilder.createSpeechUrl('https://www.example.com/sound.mp3')
)
```

### Clova.Middleware(MiddlewareOptions)
Extension IDが正しいか検証するっぽいメソッド。

```js
const clovaMiddleware = clova.Middleware({ applicationId: "YOUR_EXTENSION_ID" });
app.post('/clova', clovaMiddleware, clovaSkillHandler);
```

なおID検証しなくてもおっけー。

```js
app.post('/clova', bodyParser.json(), clovaSkillHandler);
```



### サンプルコード

以上を把握するとGitHubのREADMEのサンプルはさくっと読めるんじゃないでしょうか。

```js
const clova = require('@line/clova-cek-sdk-nodejs');
const express = require('express');
const bodyParser = require('body-parser');

const clovaSkillHandler = clova.Client
  .configureSkill()
  .onLaunchRequest(responseHelper => {
    responseHelper.setSimpleSpeech({
      lang: 'ja',
      type: 'PlainText',
      value: 'おはよう',
    });
  })
  .onIntentRequest(async responseHelper => {
    const intent = responseHelper.getIntentName();
    const sessionId = responseHelper.getSessionId();

    switch (intent) {
      case 'Clova.YesIntent':
        // Build speechObject directly for response
        responseHelper.setSimpleSpeech({
          lang: 'ja',
          type: 'PlainText',
          value: 'はいはい',
        });
        break;
      case 'Clova.NoIntent':
        // Or build speechObject with SpeechBuilder for response
        responseHelper.setSimpleSpeech(
          clova.SpeechBuilder.createSpeechText('いえいえ')
        );
        break;
    }
  })
  .onSessionEndedRequest(responseHelper => {
    const sessionId = responseHelper.getSessionId();

    // Do something on session end
  })
  .handle();

const app = new express();
const clovaMiddleware = clova.Middleware({ applicationId: "YOUR_APPLICATION_ID" });
// Use `clovaMiddleware` if you want to verify signature and applicationId.
// Please note `applicationId` is required when using this middleware.
app.post('/clova', clovaMiddleware, clovaSkillHandler);

// Or you can simply use `bodyParser.json()` to accept any request without verifying, e.g.,
app.post('/clova', bodyParser.json(), clovaSkillHandler);
```



# おわりに
今回まとめた情報は[ここ](https://github.com/line/clova-cek-sdk-nodejs/tree/master/src)のソースを読み解いたものです。
コード量も少なく、TypeScriptで書かれていますがTypeScript全く触ったことない私でもなんとなく読めたので、一度さくっと目を通してみるのも楽しいかもしれません。