音声(Alexa)でSpreadsheetに赤ちゃんのトイレの時間を記録するまで

# 概要

Alexa を通じて音声で自前の Spreadsheet に記録できるようにするまでにいろいろあったのでメモ。

ちなみに、IFTTT を使えば繋ぎこみ自体は簡単なのだが、文章になってない日本文の認識にかなり難があり、断念した。Alexa に直接仕込んだほうが遥かにいい。

これを実現するために下記の構造を作る:

```
Alexa 実機
↓
Alexa Skills Kit
↓
AWS Lambda
↓
OAuth 2.0
↓
Google Cloud Platform
↓
Google Apps Script
↓
Google Spreadsheet
```


1-1. [Spreadsheet] 記録用の Spreadseet を用意
==============

記録するためのスプレッドシートを用意しておく。中身は空っぽで良い。末尾に１行ずつ追加していく想定。

↓こういう感じ

|2018/05/05 13:41:52|うんち 大|
|-------------------|--------|

1-2. [Spreadsheet] 書き込むスクリプト作成
==============

１行追記するためのスクリプトを用意する。
`ツール` > `スクリプト エディタ` から下記のソースを追加。`Execution API` と名付けて保存。

```js:GoogleAppsScript
function appendRecords(data){
  var size = data.query_result.size;
  var mySheet = SpreadsheetApp.getActiveSheet();
  mySheet.appendRow([new Date(), 'うんち ' + size]);
}

function test_appendRecords(){
  appendRecords({
    query_result: {
      size: '大'
    }
  });
}
```

`test_appendRecords` を実行して行が追加されることを確認する。

1-3. [Spreadsheet] スクリプトをAPIとして公開する
==============

外からこのAPIを叩けるようにする。
Google Apps Script の画面から `公開` > `実行可能 API として導入...` を選択。

`バージョン` は適当に。
`スクリプトにアクセスできるユーザー` は `自分のみ` に。

`更新` のクリックでリロードみたいに画面更新されるが、自分の入力したバージョンの番号がプルダウンに加わっていることを確認する。

1-4. [Spreadsheet] 各種情報をメモしておく
==============

あとで使うので。

Google Apps Script の画面から:

- **API ID** : `公開` > `実行可能 API として導入...` の画面の `現在の API ID`
- **プロジェクトキー** : `ファイル` > `プロジェクトのプロパティ` の画面の `プロジェクト キー（サポート終了）`
- **スクリプト ID** : `ファイル` > `プロジェクトのプロパティ` の画面の `スクリプト ID`
- **OAuth スコープ** : `ファイル` > `プロジェクトのプロパティ` の画面の `スコープ` タブのURL


1-5. [Spreadsheet] OAuth2 の設定画面に飛ぶ
============================

API を叩く際の認証設定をする（ための画面に飛ぶ）。
Google Apps Script の画面から `リソース` > `Cloud Platform プロジェクト...` を選択。
ポップアップする画面の `このスクリプトが現在関連付けられているプロジェクト` 欄に `Execution API - project-id-xxxxxxxxxxxxxxxxx` のようなリンクが表示されているはず。これをクリック。


2-1. [Cloud Platform] Apps Script API を有効化
======================

Spreadsheet の API をそもそも叩けるように有効化する。
左メニューから `API とサービス` > `ライブラリ` を選択して、`Apps Script API` を検索。
１件ヒットするので、それをクリック。有効化する。

2-2. [Cloud Platform] 認証情報の作成
======================

API を叩く際の認証情報を作っておく。

左メニューから `API とサービス` > `認証情報` を選択して、`認証情報を作成` プルダウンから `OAuth クライアントID` を選択。

下記のように選択:

- アプリケーションの種類: ウェブアプリケーション
- 名前: 適当
- 制限事項:
    - 承認済みの JavaScript 生成元: `https://developers.google.com` 
    - 承認済みのリダイレクト URI: `https://developers.google.com/oauthplayground` 

JSON ができるので、ダウンロードしておく。


3-1. [OAuth 2.0 Playground] 画面開く
======================

アクセスに必要なトークンを発行するための画面を開く。
https://developers.google.com/oauthplayground

3-2. [OAuth 2.0 Playground] 認証情報設定
======================

前述2-2で発行した認証情報をセットする。
画面右上の `Auth 2.0 Configration` を開いて

- `Use your own OAuth credentials` を ON
- `OAuth Client ID` 欄に前述2-2でダウンロードしたJSON内の `client_id` の値を記入
- `OAuth Client secret` 欄に前述2-2でダウンロードしたJSON内の `client_secret` の値を記入

3-3. [OAuth 2.0 Playground] トークン発行
======================

アクセスに必要なトークンを発行する。
下記のように入力:

- Step1:
    - `https://www.googleapis.com/auth/spreadsheets` (前述1-4の`OAuth スコープ`) を入力
    - `Authorize APIs` ボタンをクリック
- Step2:
    - Authorization code: (上記のボタンで自動入力)
    - `Exchange authorization code for tokens` ボタンをクリック
    - Refresh token: (上記のボタンで自動入力)
    - Access token: (上記のボタンで自動入力)

あとで使うので **Refresh token** と **Access token** をメモしておく。

3-4. [OAuth 2.0 Playground] アクセステスト
======================

下記のように入力:

- Step3:
    - HTTP Method: POST
    - Request URI: `https://script.googleapis.com/v1/scripts/{前述1-4のプロジェクトキー}:run`
    - Enter request body クリックして下記RequestBodyを設定
    - Content-Type: `application/json`
    - `Send the request` クリック

※ `{前述1-4のプロジェクトキー}` は適宜置き換えること。

```json:RequestBody
{
  "function": "appendRecords",
  "parameters": [{
    "query_result": {
      "size": "大"
    }
  }],
  "devMode": true
}
```

シートに行が追加されればOK。


4-1. [Alexa Skills Kit] 画面開く
===================================

Alexa Skills Kit を使って対話モデルを作成する。
https://developer.amazon.com/ja/alexa-skills-kit
にアクセスしてサインインする。

※注意： サインインするメアドは必ず所有してる Alexa で使ってるメアドと同じでないといけない。同じでないと作り直しになるので注意。


4-2. [Alexa Skills Kit] スキル開発を始める
===================================

- `スキル開発を始める` クリック
- `スキルの作成` クリック
- `スキル名` は適当にあとで識別できるものを（でもあとで変えられない）
- `スキル作成時のデフォルト`: `日本語`
- `カスタム` を `選択` して `スキルを作成` クリック

4-3. [Alexa Skills Kit] 対話の設定
===================================

JSONエディター開いて下記入力:

```json
{
    "interactionModel": {
        "languageModel": {
            "invocationName": "赤ちゃんのトイレ",
            "intents": [
                {
                    "name": "AMAZON.CancelIntent",
                    "samples": [
                        "キャンセル"
                    ]
                },
                {
                    "name": "AMAZON.HelpIntent",
                    "samples": [
                        "使い方",
                        "ヘルプ"
                    ]
                },
                {
                    "name": "AMAZON.StopIntent",
                    "samples": [
                        "ありがとう",
                        "終了",
                        "ストップ"
                    ]
                },
                {
                    "name": "RecordToilet",
                    "slots": [
                        {
                            "name": "Size",
                            "type": "Size"
                        }
                    ],
                    "samples": [
                        "{Size} を記録"
                    ]
                }
            ],
            "types": [
                {
                    "name": "Size",
                    "values": [
                        {
                            "id": "小",
                            "name": {
                                "value": "小",
                                "synonyms": [
                                    "しょう"
                                ]
                            }
                        },
                        {
                            "id": "中",
                            "name": {
                                "value": "中",
                                "synonyms": [
                                    "ちゅう"
                                ]
                            }
                        },
                        {
                            "id": "大",
                            "name": {
                                "value": "大",
                                "synonyms": [
                                    "だい"
                                ]
                            }
                        }
                    ]
                }
            ]
        }
    }
}
```

- `invocationName` は「Alexa, 〇〇で大を記録」という発言の〇〇の部分になるので適宜変更。

4-4. [Alexa Skills Kit] スキルID のメモ
===================================

`エンドポイント` で `AWS LambdaのARN` を選択。
**スキルID** はあとで AWS 側で使うのでメモ。

4-5. [Alexa Skills Kit] いったんビルド
===================================

ここまでをいったんビルドしておく。


5-1. [AWS/Lambda] 関数の作成
===================================

AWSで Alexa からの要求を処理する Lambda 関数を作成する。
このときの AWS のアカウントのメアドは、Alexa のメアドと違っていても構わない。

- `関数の作成`
- `設計図` を選び、`alexa-skill-kit-sdk-factskill` を選択。
- 名前、ロールは適当に。
- `関数の作成` クリック

5-2. [AWS/Lambda] Node.JS の記述
===================================

- `アクション` > `関数のエクスポート` > `デプロイパッケージのダウンロード` でソースコードを取り出す。
- 解凍したら package.json のあるフォルダで下記のコマンド実行

```
$ npm install --save googleapis
$ npm install --save google-auth-library
```

- 完了したら `index.js` を下記のように書き換える:

```js
/* eslint-disable  func-names */
/* eslint quote-props: ["error", "consistent"]*/
/**
 * This sample demonstrates a simple skill built with the Amazon Alexa Skills
 * nodejs skill development kit.
 * This sample supports multiple lauguages. (en-US, en-GB, de-DE).
 * The Intent Schema, Custom Slots and Sample Utterances for this skill, as well
 * as testing instructions are located at https://github.com/alexa/skill-sample-nodejs-fact
 **/

'use strict';
const Alexa = require('alexa-sdk');


//=========================================================================================================================================
const {google} = require('googleapis');
const {OAuth2Client} = require('google-auth-library');

const writeToSpreadsheet = (size) => {
    let scriptId = process.env['GOOGLE_SCRIPT_ID'];
    let functionName = "appendRecords";
    const auth = new OAuth2Client(process.env['GOOGLE_CLIENT_ID'], process.env['GOOGLE_CLIENT_SECRET']);
    auth.setCredentials({
        access_token: process.env['GOOGLE_ACCESS_TOKEN'],
        refresh_token: process.env['GOOGLE_REFRESH_TOKEN']
    });
    const script = google.script('v1');
    return new Promise((resolve, reject) => {
        script.scripts.run({
            auth: auth,
            scriptId: scriptId,
            resource: {
                function: functionName,
                parameters: [{
                    "query_result": {
                        "size": size
                    }
                }],
                devMode: true
            }
        }, (err, result) => {
            if (err) {
                console.log(err);
                reject(new Error(err));
            } else {
                resolve(result);
            }
        });
    });
};
//=========================================================================================================================================



//Replace with your app ID (OPTIONAL).  You can find this value at the top of your skill's page on http://developer.amazon.com.
//Make sure to enclose your value in quotes, like this: const APP_ID = 'amzn1.ask.skill.bb4045e6-b3e8-4133-b650-72923c5980f1';
const APP_ID = process.env['ALEXA_APP_ID'];

exports.handler = function (event, context, callback) {
    const alexa = Alexa.handler(event, context, callback);
    alexa.APP_ID = APP_ID;
    const handlers = {
        'RecordToilet': function () {
            //let size = event.request.intent.slots.Size.name; //キー名
            // let size = event.request.intent.slots.Size.value;
            let size = event.request.intent.slots.Size.resolutions.resolutionsPerAuthority[0].values[0].value.id;
            console.log('size: ' + size);
            writeToSpreadsheet(size).then(() => {
                this.emit(':tell', size + 'を書き込みました');
            }).catch(error => {
                context.fail(error);
                this.emit(':tell', '書き込みに失敗しました');
            });
        },
        'AMAZON.HelpIntent': function () {
            this.emit(':tell', '大を記録、中を記録、小を記録のいずれかを言ってください');
        },
        // キャンセル（デフォルト）への返答
        'AMAZON.CancelIntent': function () {
            this.emit(':tell', 'キャンセルします');
        },
        // 対応できないアクションへの返答
        'AMAZON.StopIntent': function () {
            this.emit(':tell', 'もう一度お願いします');
        },
    };
    alexa.registerHandlers(handlers);
    alexa.execute();
};
```

5-3. [AWS/Lambda] Node.JS のアップロード
===================================

- `node_modules`, `index.js`, `package.json` を選択して右クリック > `送る(N)` > `圧縮 (zip 形式) フォルダー` で圧縮

※ 注意: このZipを解凍したときに第一階層にフォルダができる形だとエラーになる。第一階層に上記３つが並ぶように、上記のように圧縮すること。

- `関数コード` > `コード エントリ タイプ` プルダウンで `.ZIP ファイルをアップロード` を選び、`アップロード` ボタンで上記の zip ファイルをアップロードし、`保存` ボタンを押す。

5-4. [AWS/Lambda] 環境変数
===================================

Node.JS で使う環境変数を設定する。
ソース入れ替えは面倒なので、外部要因で変わりうる部分は環境変数に切り出しておくと楽。

下記の設定が必要:

|変数名|値の元|
|-----|-----|
|GOOGLE_SCRIPT_ID    |前述1-4の `スクリプト ID`|
|GOOGLE_CLIENT_ID    |前述2-2でダウンロードしたJSON内の `client_id`|
|GOOGLE_CLIENT_SECRET|前述2-2でダウンロードしたJSON内の `client_secret`|
|GOOGLE_ACCESS_TOKEN |前述3-3の `Access token`|
|GOOGLE_REFRESH_TOKEN|前述3-3の `Refresh token`|
|ALEXA_APP_ID        |前述4-4の `スキルID`|

5-5. [AWS/Lambda] ARNのメモ
===================================

**ARN** を Alexa 側にもっていくのでメモ。
画面右上の `arn:aws:lambda:ap-northeast-1:xxxxxx:function:xxxxxxxxxxxxxxxxx` の文字列。

5-6. [AWS/Lambda] トリガーの設定
===================================

- `トリガーの追加` から `Alexa Skills Kit` を選ぶ。
- `スキル ID 検証` に前述4-4の `スキルID` の値を入力。
- `保存`

6-1. [Alexa Skills Kit] ARNを設定
===================================

- 前述5-5の `ARN` を、`エンドポイント` > `AWS LambdaのARN` > `デフォルトの地域` に転記。
- `エンドポイントを保存`

6-2. [Alexa Skills Kit] ビルド
===================================

ここまでの内容をビルドして使えるようにする。

7-1. [Alexa Skills Kit] シミュレータでのテスト
===================================

- `Alexa シミュレータ` で「赤ちゃんのトイレで大を記録」と入力してみる。
- 「大を記録しました」と応答が来るのを確認
- Spreadsheet を開いてデータが追記されているのを確認


8-1. [Amazon Alexa] 実機の設定画面開く
===================================

実機にスキルを追加するため、
https://alexa.amazon.co.jp
にアクセスして Alexa 実機の設定画面を開く。

`スキル` > `有効なスキル` > `DEV スキル` タブ
に今回追加したものがあるので有効にする。



これで完了です！