前置き

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

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

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

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

  • 細かいカスタマイズができない。
    • 特に日付を設定できないのが辛い。
  • Google Home(Google Assistant)が認識した単語ごとに半角スペースが入ってしまう。

作ってみる

そんなわけで、TodoistのAPIを叩いてタスクを追加するアプリを作っていきたいと思います。

プロジェクトを作成する

Actions on Googleにアクセスして、Add/import projectをクリックすると以下のダイアログが表示されます。
Add_project-Dialog.png

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

プロジェクトの作成に成功すると、作成したプロジェクトの画面に遷移します。

Dialogflowと連携させる

プロジェクトの画面のDialogflowのカードのBUILDをクリックすると、以下のダイアログが表示されます。
Use_Dialogflow-Dialog.png

このダイアログに記載されている手順に従って進めていきます。

Dialogflowのエージェントを作成する

先程のダイアログのCREATE ACTIONS ON DIALOGFLOWをクリックすると、以下のようなDialogflowの画面に遷移します。
create_dialogflow_actions-Screen.png

利用規約を確認して、問題なければACCEPTをクリックします。
ダイアログが消えますので、言語が「Japanese」、タイムゾーンが「Asia/Tokyo」であることを確認してCREATEをクリックします。
これにてエージェントの作成は完了です。

Fulfillmentの有効化

左にあるサイドメニューのFulfillmentをクリックして、Fulfillmentの設定を行います。
スイッチをクリックしてENABLEDにして、テキトーなURLを入力します。(後ほど変更します。)
fulfillment.png

設定が完了したらSAVEボタンをクリックして、Fulfillmentを有効にします。

会話を定義する

左にあるサイドメニューのIntentsから、Intentを追加して会話を定義していきます。

起動時のIntent(Default Welcome Intent)

必須ではありませんが、デバッグする時のために、User saysに起動のトリガーとなる言葉を入力しておきます。
今回は、「起動」と入力することでアプリが立ち上がるようにしました。

次にResponseセクションにあるText responseカードを、ゴミ箱アイコンをクリックして削除します。
その後、ADD MESSAGE CONTENTText responseとクリックして、新しくText responseカードを追加します。
テキストボックスに喋らせたい内容を入力して、SAVEをクリックしたら編集完了です。
Default_Welcome_Intent.png

タスクについてやりとりするIntent

をクリックして新しいIntentを追加します。
Intent nameは「add_task」にしました。

まず、ユーザーが「○○(タスク名)を追加」と言ったら、○○にあたる内容をタスクと見做すようにします。
user_says.png

次に、Actionの定義です。
action.png

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

  • Required - チェックをつける
    • チェックをつけると、ユーザーが、このアクションを満たす適切なフレーズを言うまで、トリガーがかかりません。
  • Parameter name - task
  • Entity - @sys.any
  • 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でデプロイしました。
それぞれの使い方はこちらの記事に丁寧に書かれていますので、詳しくはそちらをご覧ください。

こちらのサンプルも参考にしながら以下のように書きました。

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を使います。
表示されたURLをFulfillmentに設定して、右側のエリアでテストします。
debug.png

無事レスポンスが返ってきました!🎉

Integrationsの設定

左にあるサイドメニューのIntegrationsをクリックして、Google Assistantをクリックします。
以下のダイアログが表示されるので、TESTをクリックします。
google_assistant.png

このような表示になれば、Google Homeから呼び出すことが可能です!
view.png

Google Homeに「テスト用アプリにつないで」と話すと、アプリが立ち上がります。

最後に

今回はホントにシンプルなタスク追加でしたが、まだまだカスタマイズする余地はあると思っています。
タスク追加をさらに掘り下げることもできますし、追加以外の機能、例えばタスクの確認などもできます。
Todoistに限らず、音声操作を最大限活かせるようなシチュエーションで使えるアプリを作れたらいいですね!

参考リンク

Actions on Googleでapi.aiを使ってGoogle Homeに何か言わせてみる

Build Your First App with Dialogflow

Actions on Google と AWS Lambda で Google Home から Slack にポストする

Heroku+Node.jsの使い方(1)準備

Getting Started on Heroku with Node.js

supermamon/apiai-nodejs-webhook-sample

ngrokでlocalhostに外部からアクセスできるようにする

TodoistのREST APIリファレンス