Clova SDK を使ってカスタムスキルを heroku にデプロイして動かす

2018年7月12日に [Clova Extensions Kit（CEK）](https://clova-developers.line.me/) がリリースされました。

チュートリアルは既に皆さまやっているようなので置いといて、
公式の [Clova SDK](https://github.com/line/clova-cek-sdk-nodejs) を使ってサンプルスキルを heroku にデプロイしてみました。
実装は TypeScript を用いています。

CEK の基礎や CEK の使い方などは @imajoriri さんの以下の記事が詳しいです。

- [[入門]Clovaスキル(CEK)は作りながら覚えて行く（前提知識編）](https://qiita.com/imajoriri/items/8d511cdfde8cb731ff82)
- [[入門]Clovaスキル(CEK)は作りながら覚えて行く（インテント、スロットなど登録編）](https://qiita.com/imajoriri/items/604ff74c4e81859771e2)

# 開発環境

```bash
$ heroku -v
heroku/7.7.1 darwin-x64 node-v10.6.0

$ node -v
v8.11.2

$ npm -v
6.1.0

$ git --version
git version 2.15.2 (Apple Git-101.1)
```

# 事前準備

- git
- heroku CLI
- node

# CEK で Extension を作成

詳細は既に皆さまが書かれておりますので割愛します。

1. `プロバイダー` を登録
1. `チャネル` を作成
1. `基本設定` を入力
    1. Extension ID は後ほど使いますのでメモメモ
1. `サーバー設定` はとりあえずスルー
    1. 後でやります
1. `配布情報` は今回はリリースしないのでスルー
1. `個人情報の保護および規約同意` も今回はリリースしないのでスルー
1. `対話モデル` へ

## 対話モデル

<img width="1024" alt="custom-extension.png" src="https://qiita-image-store.s3.amazonaws.com/0/53393/c559c4c3-ba26-da26-224d-366f268d2a74.png" style="border: 1px solid #000">

- カスタムインテントには 「KinokoIntent」を1個追加
- サンプル発話にはとりあえず「きのこ」と「キノコ」を設定

## テスト

<img width="1024" alt="custom-extension-tet.png" src="https://qiita-image-store.s3.amazonaws.com/0/53393/713f9eda-b587-3dbc-b7c2-925e97a2efe3.png">

- サーバーの設定はしてないので、応答はありません。

# スキルの実装側を作成

基本的に [README.md](https://github.com/line/clova-cek-sdk-nodejs) を参照しました。
ゴールは https://github.com/chibi929/hello-clova に置きました。

## プロジェクト作成

```bash
$ mkdir hello-clova
$ cd hello-clova

# リモートリポジトリはあとで `heroku CLI` が設定してくれるので init だけで OK
$ git init

# `npm init` は適当にスキップなり入力なり
$ npm init

$ npm install --save @line/clova-cek-sdk-nodejs body-parser express
$ npm install --save--dev typescript

# `src/` には後でソースコードを置きます。
$ mkdir src
```

### package.json

```json:package.json
{
  "name": "hello-clova",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "engines": {
    "node": "8.x"
  },
  "dependencies": {
    "@line/clova-cek-sdk-nodejs": "^1.0.1",
    "body-parser": "^1.18.3",
    "express": "^4.16.3"
  },
  "devDependencies": {
    "typescript": "^2.9.2"
  }
}
```

### tsconfig.json

TypeScript 用に tsconfig.json を用意

```json:tsconfig.json
{
  "compileOnSave": false,
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "lib",
    "noImplicitReturns": true,
    "sourceMap": true,
    "lib": ["es6"]
  },
  "include": [
    "src/"
  ]
}
```
たしか `firebase init` から生まれた `tsconfig.json` を使い回していたような。。。

### ソースコード

[Clova SDK のリポジトリ](https://github.com/line/clova-cek-sdk-nodejs)にあった [example/echo](https://github.com/line/clova-cek-sdk-nodejs/tree/master/examples/echo) を参考に TypeScript のコードを書く

```diff:src/index.ts
import { Client, SpeechBuilder, Middleware } from '@line/clova-cek-sdk-nodejs';
import * as express from 'express';
const app = express();

const launchHandler = async responseHelper => {
  responseHelper.setSimpleSpeech(
    SpeechBuilder.createSpeechText('おはよう')
  );
};

const intentHandler = async responseHelper => {
  const intent = responseHelper.getIntentName();
  const sessionId = responseHelper.getSessionId();

  switch (intent) {
+   case 'KinokoIntent':
+     responseHelper.setSimpleSpeech(
+       SpeechBuilder.createSpeechText('きのこのこのこ元気の子')
+     );
+     break;
    case 'Clova.YesIntent':
      responseHelper.setSimpleSpeech(
        SpeechBuilder.createSpeechText('はいはい')
      );
      break;
    case 'Clova.NoIntent':
      responseHelper.setSimpleSpeech(
        SpeechBuilder.createSpeechText('いえいえ')
      );
      break;
    default:
      responseHelper.setSimpleSpeech(
        SpeechBuilder.createSpeechText('なんなん')
      );
      break;
  }
};

const sessionEndedHandler = async responseHelper => { };

- const clovaHandler = Client
+ // TypeScript だと RequestHandler 型にしないと怒られるのでとりあえず any で逃げる
+ const clovaHandler: any = Client
    .configureSkill()
    .onLaunchRequest(launchHandler)
    .onIntentRequest(intentHandler)
    .onSessionEndedRequest(sessionEndedHandler)
    .handle();

const clovaMiddleware = Middleware({ applicationId: process.env.APPLICATION_ID });

app.post('/clova', clovaMiddleware, clovaHandler);

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server running on ${port}`);
});
```

- KinokoIntent のハンドリング
- 型違いで怒られたので修正

### ビルド

**その前に package.json を修正**

```diff:pacjage.json
{
  "name": "hello-clova",
  "version": "1.0.0",
  "description": "",
- "main": "index.js",
+ "main": "lib/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
+   "build": "tsc"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@line/clova-cek-sdk-nodejs": "^1.0.1",
    "body-parser": "^1.18.3",
    "express": "^4.16.3"
  },
  "devDependencies": {
    "typescript": "^2.9.2"
  }
}
```

- `tsconfig.json` により `lib/` 以下にビルドしたものが生成される
- `npm run build` を使えるようにする

**ビルド**

```bash
$ npm run build

> hello-clova@1.0.0 build /private/tmp/hello-clova
> tsc

$ ls
lib		node_modules	package.json	src		tsconfig.json
```

- こうなれば OK

#### トラブルシュート

```bash
$ npm run build

> /private/tmp/hello-clova
> tsc

node_modules/@line/clova-cek-sdk-nodejs/dist/types/types.d.ts:1:8 - error TS1192: Module '"/private/tmp/hello-clova/node_modules/@types/express/index"' has no default export.

1 import express from 'express';
         ~~~~~~~


node_modules/@line/clova-cek-sdk-nodejs/dist/types/verifierMiddleware.d.ts:1:8 - error TS1192: Module '"/private/tmp/hello-clova/node_modules/@types/express/index"' has no default export.

1 import express from 'express';
         ~~~~~~~
```

と、怒られたが、
[Clova SDK の PR#2](https://github.com/line/clova-cek-sdk-nodejs/pull/2) を見て解決。
言われるがまま、以下のように `tsconfig.json` を修正

**tsconfig.json を修正**

```diff:tsconfig.json
{
  "compileOnSave": false,
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "outDir": "lib",
    "noImplicitReturns": true,
    "sourceMap": true,
+   "allowSyntheticDefaultImports": true,
    "lib": ["es6"]
  },
  "include": [
    "src/"
  ]
}
```

# heroku の準備

## ログイン

```bash
$ heroku login
# ID(メールアドレス) 入力
# パスワード入力
```

## heroku app 作成

```bash
$ heroku create
# ここでリモートリポジトリが設定される
```

### Procfile を準備

```:Procfile
web: node lib/index.js
```

### 環境変数を準備

```bash
# process.env.APPLICATION_ID で取得できるようになる
$ heroku config:set APPLICATION_ID="さっきメモメモしといた Extension ID"

# デプロイ後、devDependencies もインストールしたいので false にする
$ heroku config:set NPM_CONFIG_PRODUCTION=false
```

### デプロイ後のビルドコマンド追加

```diff:package.json
{
  "name": "hello-clova",
  "version": "1.0.0",
  "description": "",
  "main": "lib/index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "tsc",
+   "heroku-postbuild": "npm run build"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@line/clova-cek-sdk-nodejs": "^1.0.1",
    "body-parser": "^1.18.3",
    "express": "^4.16.3"
  },
  "devDependencies": {
    "typescript": "^2.9.2"
  }
}
```

- `heroku-prebuild` は heroku 上で npm install が完了したら実行される

### heroku へデプロイ

```bash
$ git add Procfile package.json src tsconfig.json
$ git commit -m "Add project"
$ git push heroku master
  ~略~
remote:        https://${HEROKU_APP_NAME}.herokuapp.com/ deployed to Heroku        
remote: 
remote: Verifying deploy... done.        
To https://git.heroku.com/${HEROKU_APP_NAME}.git
   2433fb3..55d6dfc  master -> master
```

- `remote:        https://${HEROKU_APP_NAME}.herokuapp.com/ deployed to Heroku`
    - こんなの出れば OK
    
## アクセス

`https://${HEROKU_APP_NAME}.herokuapp.com/` にブラウザでアクセスすると

> Cannot GET /

こんな感じですかね？
できてない場合は `heroku logs` とかでログ見ないとだー。

# CEK の `サーバー設定`

<img width="1024" alt="cek-server-config.png" src="https://qiita-image-store.s3.amazonaws.com/0/53393/afb80e86-9e4c-20f2-19e8-43a6c1694231.png">

- `https://${HEROKU_APP_NAME}.herokuapp.com/clova` を設定
    - これは `index.ts` で `POST /clova` を作ったため

# 最後のテスト！

<img width="692" alt="result.png" src="https://qiita-image-store.s3.amazonaws.com/0/53393/07f1d5db-fac3-0d64-c4e7-cfba0f025e00.png">

# まとめ

もともと heroku の知識があったため、
サンプルの JS を heroku にデプロイして Clova の繋げるまでは 18分 でできました。

その後、記事書きながら TypeScript に置き換えるのに、時間を取られました…

Clova SDK や CEK は、それなりに理解しやすかったです。

(単純に動かすだけなら簡単！！)

## 記事内に貼ったリンクのまとめ

- https://github.com/chibi929/hello-clova
- [Clova Extensions Kit（CEK）](https://clova-developers.line.me/)
- [Clova SDK](https://github.com/line/clova-cek-sdk-nodejs)
    - [example/echo](https://github.com/line/clova-cek-sdk-nodejs/tree/master/examples/echo)
- [[入門]Clovaスキル(CEK)は作りながら覚えて行く（前提知識編）](https://qiita.com/imajoriri/items/8d511cdfde8cb731ff82)
- [[入門]Clovaスキル(CEK)は作りながら覚えて行く（インテント、スロットなど登録編）](https://qiita.com/imajoriri/items/604ff74c4e81859771e2)