GoogleHomeからTodoistにタスクを追加する

#前置き
**※長くなってしまったので、さっさとタイトルの内容について知りたい方は飛ばしてください🙇**

Google Homeが日本に上陸して早2ヶ月…。
みなさん、バリバリ活用してますか！？！？！？
私はGoolgePlayMusicで音楽を流したり、Chromecastと連携してYouTubeで動画を観たりしています。
やはり声だけで操作できるというのは面白いし便利ですよね。
よくわかりませんでした、を連発されてもどかしい時もありますが…。

私も一応エンジニアなので、Google Homeを使って何かアプリを作りたいな〜と漠然と考えていました。
そんななか、Todoistのタスクを整理していた時に思ったのです。
**声だけでタスクを追加できれば捗るのでは？**、と。
タスクの追加はとにかく早さが大事(あとでやろうと思っても忘れる)なので、
たとえ両手が塞がっていてもタスクの追加ができるのは大きなメリットだと考えました。

ちなみに、単純なタスクの追加であれば、[IFTTT](https://ifttt.com/)と連携させることで実現が可能ですが、以下のようなデメリットがあります。

* 細かいカスタマイズができない。
    - 特に日付を設定できないのが辛い。
* Google Home(Google Assistant)が認識した単語ごとに半角スペースが入ってしまう。
    - Google HomeとIFTTTを連携させるのは、[他の方が書かれています](https://qiita.com/search?q=GoogleHome+IFTTT)ので割愛します。

#作ってみる
そんなわけで、TodoistのAPIを叩いてタスクを追加するアプリを作っていきたいと思います。
##プロジェクトを作成する
[Actions on Google](https://console.actions.google.com/)にアクセスして、`Add/import project`をクリックすると以下のダイアログが表示されます。
<img width="568" alt="Add_project-Dialog.png" src="https://qiita-image-store.s3.amazonaws.com/0/106546/2bf955c7-178d-653c-d909-76054766f391.png">

プロジェクト名と国を入力して、`CREATE PROJECT`をクリックして、プロジェクトを作成します。
今回は、プロジェクト名を「Verbalist」、国を「Japan」としました。

プロジェクトの作成に成功すると、作成したプロジェクトの画面に遷移します。
##Dialogflowと連携させる
プロジェクトの画面のDialogflowのカードの`BUILD`をクリックすると、以下のダイアログが表示されます。
<img width="723" alt="Use_Dialogflow-Dialog.png" src="https://qiita-image-store.s3.amazonaws.com/0/106546/866e8964-050c-d01d-5d5f-b92db6ddf451.png">

このダイアログに記載されている手順に従って進めていきます。
###Dialogflowのエージェントを作成する
先程のダイアログの`CREATE ACTIONS ON DIALOGFLOW`をクリックすると、以下のようなDialogflowの画面に遷移します。
<img width="1272" alt="create_dialogflow_actions-Screen.png" src="https://qiita-image-store.s3.amazonaws.com/0/106546/da1460ce-9fd5-5aa6-185f-23b31286958c.png">

利用規約を確認して、問題なければ`ACCEPT`をクリックします。
ダイアログが消えますので、言語が「Japanese」、タイムゾーンが「Asia/Tokyo」であることを確認して`CREATE`をクリックします。
これにてエージェントの作成は完了です。
###`Fulfillment`の有効化
左にあるサイドメニューの`Fulfillment`をクリックして、`Fulfillment`の設定を行います。
スイッチをクリックして`ENABLED`にして、テキトーなURLを入力します。(後ほど変更します。)
<img width="894" alt="fulfillment.png" src="https://qiita-image-store.s3.amazonaws.com/0/106546/704479dd-ee2a-9ad4-3b0b-188c67896275.png">

設定が完了したら`SAVE`ボタンをクリックして、`Fulfillment`を有効にします。
##会話を定義する
左にあるサイドメニューの`Intents`から、Intentを追加して会話を定義していきます。
###起動時のIntent(`Default Welcome Intent`)
必須ではありませんが、デバッグする時のために、`User says`に起動のトリガーとなる言葉を入力しておきます。
今回は、「起動」と入力することでアプリが立ち上がるようにしました。

次に`Response`セクションにある`Text response`カードを、ゴミ箱アイコンをクリックして削除します。
その後、`ADD MESSAGE CONTENT`＞`Text response`とクリックして、新しく`Text response`カードを追加します。
テキストボックスに喋らせたい内容を入力して、`SAVE`をクリックしたら編集完了です。
<img width="1258" alt="Default_Welcome_Intent.png" src="https://qiita-image-store.s3.amazonaws.com/0/106546/e3481788-b6a1-8800-8d7f-3aa5ff1980ea.png">
###タスクについてやりとりするIntent
`＋`をクリックして新しいIntentを追加します。
`Intent name`は「add_task」にしました。

まず、ユーザーが「○○(タスク名)を追加」と言ったら、○○にあたる内容をタスクと見做すようにします。
<img width="630" alt="user_says.png" src="https://qiita-image-store.s3.amazonaws.com/0/106546/0c1a379f-3e4b-0fd5-52a8-45007ee855f1.png">

次に、`Action`の定義です。
<img width="617" alt="action.png" src="https://qiita-image-store.s3.amazonaws.com/0/106546/0386930a-7abe-1767-3fb1-f1353cb21208.png">

先程定義した`task`パラメータについて以下のように定義します。

* Required - __チェックをつける__
    - チェックをつけると、ユーザーが、このアクションを満たす適切なフレーズを言うまで、トリガーがかかりません。
* Parameter name - __task__
* Entity - __@sys.any__
    * [Dialogflowのドキュメント](https://dialogflow.com/docs/reference/system-entities)が詳しいです。
* Value - __$task__
* Prompts - __タスクを教えてください__
    * Dialogflowが発話を検知するまで、このフレーズがユーザーに対して繰り返し発話されます。

また、日付のパラメータを`date`として以下のように定義します。

* Required - __チェックをつける__
* Parameter name - __date__
* Entity - __@sys.date-time__
* Value - __$date__
* Prompts - __予定日時を教えてください__

このように追加の入力(`date`)を`Required`として定義することで、新しくIntentを作らなくても、続けて質問をすることができます。

最後に、`Fulfillment`セクションの`Use webhook`と、`Actions on Google`セクションの`End conversation`にチェックを入れて完了です。
##Fulfillmentを構築する
今回はNode.jsで作ったアプリをHerokuでデプロイしました。
それぞれの使い方は[こちらの記事](http://info-i.net/heroku-node-js)に丁寧に書かれていますので、詳しくはそちらをご覧ください。

[こちらのサンプル](https://github.com/supermamon/apiai-nodejs-webhook-sample/blob/master/index.js)も参考にしながら以下のように書きました。

```js:index.js
// モジュールのインポート
var express = require('express');
var bodyParser = require('body-parser');
var request = require('request');

// 開発で使うポート番号
const LOCAL_PORT_NUMBER = 5000;

// ----初期化と設定----
var app = express();
app.set('port', (process.env.PORT || LOCAL_PORT_NUMBER));
app.set('x-powered-by', false);
app.set('case sensitive routing', true);
app.set('strict routing', true);
app.use(bodyParser.json());

// VerbalistのFulfillment
app.post('/google_home/verbalist', function (req, res, next) {
  console.log('=====[REQUEST]====');
  console.log(req.body);

  // リクエストに必要なパラメータが含まれていない場合は、以降の処理を中止する
  if (!req.body || !req.body.result || !req.body.result.parameters) {
    return res.status(400).send('No parameters.');
  }

  // タスクの内容を取得する
  var task = req.body.result.parameters.task;
  console.log('=====[TASK]====');
  console.log(task);
  // 単語が半角スペースで区切られているのでそれを削除
  var trimmedTask = task.replace(/ /g, '');

  // 予定日時を取得する(JSTをUTCに変換する)
  var jstDateTime = Date.parse(req.body.result.parameters.date);
  var utcDate = new Date(jstDateTime - 1000 * 60 * 60 * 9);
  console.log(utcDate);

  var options = {
    // MEMO: TOKENは適当な文字列を入れてください
    uri: 'https://beta.todoist.com/API/v8/tasks?token=' + TOKEN,
    headers: {
      'Content-type': 'application/json',
    },
    json: {
      'content': trimmedTask,
      'due_datetime': utcDate
    }
  };

  // POSTする
  request.post(options, function (error, response, body) {
    // 返答内容
    var speech;
    // (ディスプレイがあれば)ディスプレイに表示する内容
    var displayText;

    if (error) {
      console.log('=====[ERROR]====');
      console.log(error);

      speech = 'タスクの追加に失敗しました。';
      displayText = 'タスクの追加に失敗しました。';
    } else {
      console.log('=====[BODY]====');
      console.log(body);

      speech = 'タスク、' + body.content + '、を追加しました。';
      displayText = 'タスク名：' + body.content + '\n' + '予定日時：' + body.due.datetime;
    }

    // レスポンスを送る
    res.json({
      source: 'add_task',
      speech: speech,
      displayText: displayText
    });
  });
});

// 第一引数にポート番号、第二引数にコールバック関数を指定して、サーバを起動
var server = app.listen(app.get('port'), function () {
  console.log('http server is running...');

  // すでに終了しているかどうかのフラグ
  var isFinished = false;

  process.on('SIGTERM', () => {
    // すでに終了している場合は何もしない
    if (isFinished) {
      return;
    }
    isFinished = true;

    console.log('http server is closing...');

    // サーバを停止する
    server.close(function () {
      console.log('http server closed.');

      // 正常終了
      process.exit(0);
    });
  });
});

// サーバのエラーを監視する
server.on('error', function (err) {
    console.error(err);

    // 異常終了
    process.exit(1);
});
```

ローカルで動作を確認したいところですが、localhostはFulfillmentには指定できないので、[ngrok](https://ngrok.com/)を使います。
表示されたURLをFulfillmentに設定して、右側のエリアでテストします。
<img width="369" alt="debug.png" src="https://qiita-image-store.s3.amazonaws.com/0/106546/3e8aeae6-d42c-7331-eca5-05c4a834e082.png">

無事レスポンスが返ってきました！🎉
##`Integrations`の設定
左にあるサイドメニューの`Integrations`をクリックして、Google Assistantをクリックします。
以下のダイアログが表示されるので、`TEST`をクリックします。
<img width="820" alt="google_assistant.png" src="https://qiita-image-store.s3.amazonaws.com/0/106546/0538ad4a-271e-7e33-f080-27135f3e6071.png">

このような表示になれば、Google Homeから呼び出すことが可能です！
<img width="831" alt="view.png" src="https://qiita-image-store.s3.amazonaws.com/0/106546/ab8aa27a-a053-39ee-037b-f610e39b96bd.png">

Google Homeに「テスト用アプリにつないで」と話すと、アプリが立ち上がります。
#最後に
今回はホントにシンプルなタスク追加でしたが、まだまだカスタマイズする余地はあると思っています。
タスク追加をさらに掘り下げることもできますし、追加以外の機能、例えばタスクの確認などもできます。
Todoistに限らず、音声操作を最大限活かせるようなシチュエーションで使えるアプリを作れたらいいですね！
#参考リンク
[Actions on Googleでapi.aiを使ってGoogle Homeに何か言わせてみる](https://qiita.com/syarihu/items/53ea1a65f481f8121109)

[Build Your First App with Dialogflow](https://developers.google.com/actions/dialogflow/first-app)

[Actions on Google と AWS Lambda で Google Home から Slack にポストする](http://tech.unifa-e.com/entry/2017/10/16/103458)

[Heroku+Node.jsの使い方（１）準備](http://info-i.net/heroku-node-js)

[Getting Started on Heroku with Node.js](https://devcenter.heroku.com/articles/getting-started-with-nodejs#introduction)

[supermamon/apiai-nodejs-webhook-sample](https://github.com/supermamon/apiai-nodejs-webhook-sample)

[ngrokでlocalhostに外部からアクセスできるようにする](https://qiita.com/yamagen0915/items/e516dee422c663b823c7)

[TodoistのREST APIリファレンス](https://developer.todoist.com/rest/v8/)