Dialogflowを使ってGoogle Homeにメールを送信してもらう

試作シリーズ。なんか出来てちょっとでもうれしいことをとりあえず作ってみようということで作ってみました！

Actions on Google から Dialogflow を利用して、Cloud Functions for Firebase のバックエンドにリクエストを投げて、バックエンドと Firebase Database でやり取りをさせて、バックエンドからメールを送信するという、声だけでメールを送るアプリを作りました。

声でメールを送るというのは、[標準機能](https://assistant.google.com/explore/c/14/?hl=ja)や [IFTTT](https://ifttt.com/login) とかで Google Assistant と連携してできることではあるのですが、宛先指定のしやすい、何者にも縛られない俺だけの Google Home アプリにしたくて。。。と思い立ちました。（ IFTTT だとワードががっちり固定されて使うのが難しかったりするので。。）

あと今回はメールですが、送り先を Slack だののトークアプリにしたりと応用できるかなとか思ってたりしてます！


#概要

下記のような会話フローを経て、Google Home にメールを送ってもらいます。

ユ  = ユーザー
H = Google Home

ユ：「OK, Google. ”ボイスメール”を起動して」
H：「わかりました。 ボイスメールのテストバージョンです。」
H：「こんにちは。メッセージを送りたい相手を教えてください」
ユ：「どき（さん）」
H：「どきかずのりさんに、送りたいメッセージを教えてください。」
ユ：「こんにちは」
H：「どきかずのりさんにメッセージを送信しました。」

※メール送信元は固定のアドレス（今回はGmailのアドレスを利用。）になります。

途中で中断したい、挙動がおかしくなった場合には、Google Home 自身の中断アクション（上面部をタップ）を行うのが一番手軽です。

メール送信先は、あらかじめ Dialogflow と Firebase Database に登録してある宛先にしか送信できないようにしています。感覚としては、Dialogflow の Entities で”相手”の特定をし、Firebase Database で”相手の情報”を取得するようにしています。


#実装手順

実装手順は下記のようになります。

- Actions on Google プロジェクト作成
- Dialogflow 作成①
 - Agent 作成
 - Entities 作成
 - Intents 作成
- Cloud Functions for Firebase 作成
 - JSON データインポート
- Dialogflow 作成②
- Actions on Google プロジェクト設定
- 実機（シミュレーション）テスト

以前に Dialogflow での基本的な Actions on Google の作り方記載した記事があるので、同じ内容の手順は省いて解説していきます。Dialogflow を使った Actions on Google の作り方に慣れていない人は下記記事の解説を併用して参照してください。

[Dialogflow と Firebase Cloud Functions で Actions On Google 作り](https://qiita.com/doki_k/items/11f8a23d71dce59409da)


#実装

##Actions on Google プロジェクト作成

※過去記事参照。

##Dialogflow 作成①
###Agent 作成

※過去記事参照。

###Entities 作成
人の呼ばれ方は様々だったりします。
例えば、どき（姓）、かずのり（名）、どきかずのり（姓名）、どっきー（あだ名①）、どきどき＠（あだ名②）などの呼ばれ方が人によってあると思います。
その様々な呼ばれ方を一意の値にして、バックエンド側に渡すために、Entities を作成します。

Entities 名：SendList

|Key|Synonym|
|:---|:---|
|4567|4567, どき, かずのり, どきかずのり, どっきー, どきどきあっとまーく|
|1234|1234, さとう, いちろう, さとういちろう, さといち, ぶちょう|
|2345|2345, すずき, じろう, すずきじろう, ちーふ|
|3456|3456, たなか, はなこ, たなかはなこ, はな, たかはなこ|

Keyに使用している数値は、社員番号など一意なものとします。

上記のように Entities を作成すれば、一意の値をバックエンドに渡すことができます。

ただ名字が同じ人がいる場合は、「言い方を変えてください」など一工夫しなければいけません。また同姓同名の人がいる場合、いっそのこと社員番号で音声認識させようとするなどの運用回避をしましょう。

###Intents 作成
今回必要な Intent は、送信先の名前入力に関する Intent、送信したいメッセージ入力に関する Intent です。

送信先の名前入力の Intent は、冒頭に書いた会話フローの ”ユ：「どき（さん）」” の部分になります。
メールを送信したい相手を Google Home に向かって話した時に反応する Intent となります。

人によってさん付け、くん付けする可能性を考慮して、”どき”、”どきさん”、”どきくん”と UserSays
 に入力します。”どき”は Entities の Synonym に登録されている値なので、”Parameter”として登録されます。

送信したいメッセージ入力の Intent は、メッセージには何が入ってくるかわからないので、”Default Fallback Intent”を流用します。（後で）”Use webhook”にチェックを入れるだけです。


##Cloud Functions for Firebase 作成

基本的な作り方は過去記事と同じです。今回は前回のものに加え、Firebase Database とのやり取りとメール送信に関する処理を加えていきます。

まず、Firebase Database にインポートする JSON ファイルを定義しておきましょう。下記のように作成します。

```json:sendlist.json
{
    "data":{
        "sendto":{
            "4526":{
                "code": "4567",
                "name": "どきかずのり",
                "mail": "doki@ドメイン"
            },
            "1234":{
                "code": "1234",
                "name": "さとういちろう",
                "mail": "sato@ドメイン"
            },
            "2345":{
                :
             （中略）
                :
            }
        }
    }
}
```

上記 JSON ファイルを Firebase Database にインポートすると下図のようになります。
<img width="945" alt="Screen Shot 2018-02-15 at 15.50.32.png" src="https://qiita-image-store.s3.amazonaws.com/0/92506/1c6fb3e9-31f2-a021-fdb0-3e6cb0bce21e.png">


メールの送信は、Node.js のモジュールである ”nodemailer” を使用します。

メール送信のソースは、[nodemailer 公式](https://nodemailer.com/about/) の情報を参考に作成してください。
うちの会社のアカウントは2段階認証を利用して認証を行っているので、アプリパスワードを取得する必要がありました。アプリパスワードは、Google アカウントの設定から生成してきます。
<img width="775" alt="Screen Shot 2018-02-15 at 16.21.42.png" src="https://qiita-image-store.s3.amazonaws.com/0/92506/76201a48-ae15-80fb-48e0-6bfb15d7c5f3.png">


2段階認証の設定を行っていない場合は、アプリでメールを送信するところまで実行した時に警告メールがメールアカウントに来ているので、そのメール内リンクから”安全性の低いアプリへのアクセス許可”の設定を行ってください。詳細については調べていませんが（書いてあるまんまだと思いますが）、それでメール送信ができるようになります。
<img width="611" alt="Screen Shot 2018-02-15 at 16.24.10.png" src="https://qiita-image-store.s3.amazonaws.com/0/92506/316f4221-9149-a96f-bf71-0bf48857fc45.png">
<img width="747" alt="Screen Shot 2018-02-15 at 16.26.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/92506/49c98aef-cd83-3b2b-ac30-7b5a1785c013.png">

ソースコードは下記のようになりました。

```javascript:index.js
'use strict';

process.env.DEBUG = 'actions-on-google:*';
// Dialogflow用
const { DialogflowApp } = require('actions-on-google');
// Cloud Functions用
const functions = require('firebase-functions');
// Firebase Database用
const admin = require('firebase-admin');
// メール送信用
const nodemailer = require('nodemailer');

admin.initializeApp(functions.config().firebase);
admin.database.enableLogging(true);

exports.sendmldlfAction = functions.https.onRequest((request, response) => {
    const app = new DialogflowApp({ request, response });
    console.log('Request headers: ' + JSON.stringify(request.headers));
    console.log('Request body: ' + JSON.stringify(request.body));

    // Fulfill action business logic
    // メール送信先確認
    function responseSet(app) {
        let speechStr = "";
        let sendTo = JSON.stringify(request.body.result.parameters.SendList);
        sendTo = sendTo.replace(/"/g, "");
        // 送信先社員番号キープ
        app.data = { name: sendTo };

        let db = admin.database();
        let ref = db.ref("/data");
        ref.child("sendto").orderByChild("code").equalTo(sendTo).once("value", function (snapshot) {
            let sendToName = '';
            snapshot.forEach(function (snapinfo) {
                // console.log(snapinfo.key);
                sendToName = snapinfo.val().name;
            });
            speechStr = sendToName + 'さんに、送りたいメッセージを教えてください。';
            app.ask(speechStr);
        });
    }

    // メール送信先取得
    function responseSend(app) {
        let speechStr = "";
        // 入力音声そのままを取得
        let sendMessage = app.getRawInput();

        let db = admin.database();
        let ref = db.ref("/data");
        ref.child("sendto").orderByChild("code").equalTo(app.data.name).once("value", function (snapshot) {
            let sendToName = '';
            let sendToMail = '';
            snapshot.forEach(function (snapinfo) {
                console.log(snapinfo.key);
                sendToName = snapinfo.val().name;
                sendToMail = snapinfo.val().mail;
            });

            //Gmail送信
            let transporter = nodemailer.createTransport({
                host: 'smtp.gmail.com',
                port: 465,
                secure: true, // SSL
                auth: {
                    user: 'メールアドレス',
                    pass: 'パスワード'
                }
            });
            
            let mailOptions = {
                from: 'メールアドレス',
                to: sendToMail,
                subject: 'GoogleHome経由メッセージ',
                html: sendMessage
            };

            transporter.sendMail(mailOptions, (err, res) => {
                // 送信失敗
                if(err){
                    return console.log(err);
                }
                // 送信成功
                console.log('Message sent: ' + res.message);
            });

            speechStr = sendToName + 'さんにメッセージを送信しました。';
            app.tell(speechStr);
        });
    }

    const actionMap = new Map();
    actionMap.set('action.set', responseSet);
    actionMap.set('input.unknown', responseSend);

    app.handleRequest(actionMap);
});

```

送信先名前入力の Intent は、”responseSet”ファンクションを、送信したいメッセージ入力の Intent は、”responseSend”ファンクションを実行するように actionMap をセットします。

”responseSet”ファンクションでは、社員番号から宛先となる相手の名前を取得するために、”responseSend”ファンクションでは、社員番号から宛先となるメールアドレスを取得するために Firebase Database にアクセスします。

Firebase Database からのデータ取得の仕方などは[公式のリファレンス](https://firebase.google.com/docs/database/admin/retrieve-data?hl=ja)を参照してください。

”nodemailer”のソース部は、2段階認証の場合、”user: 'メールアドレス'”の下の”pass: 'パスワード'”にはアプリパスワードを設定する点に注意してください。

他に特筆すべき点として、前回に話しかけた内容（同じ会話セッション内のみ）を保持しておく、”app.data = { name: sendTo }”や、話しかけたままの内容を取得する”app.getRawInput()”があるのでチェックしてみてください。


##Dialogflow作成②

上記 index.js のソースコードの”actionMap.set”にて記載した”action.set”と”input.unknown”ですが、送信先名前入力の Intent の Action 項目に”action.set”を、送信したいメッセージ入力の Intent の Action 項目には、”input.unknown”をそれぞれ設定します。（”Default Fallback Intent”を流用している場合は”input.unknown”は既に設定されているものとなります。）

Fulfillment の設定に関しては、過去記事の通りです。


##Actions on Google プロジェクト設定

※過去記事を参照。


以上で実装完了です。


#まとめ

[以前の記事](https://qiita.com/doki_k/items/11f8a23d71dce59409da)の内容から、Firebase Database と nodemailer を利用しただけで特に真新しい部分はないですが、実装の一例として参照していただければと思います。細かいポイントですが、JSON や Entities に登録する名前データはなるべくひらがなにしておいた方がいいのかなと思います。（漢字にしていると予期せぬ呼び方、認識をされることがあります。）

また、テストバージョンのままの Google Home アプリは7日間で有効期限が切れるので、定期的に”TEST DRAFT”ボタンを押下して、アプリを上げ直してあげる必要があります。めんどくさいですが、仕様です。。（どうにかして…！）
cronで定期的にgactionコマンドをやり直す方法であれば、自動化でその手間は省けるんじゃね？って記事をみたので、Actions SDKで当アプリを作ろうとしたのですが、行ってほしい Intent にうまく入らなかったり、Synonym をうまく働かせられなかったしたので、泣く泣く断念しました。。。（もう少しActions SDKを理解すればわかりませんが…）

Googleさん。。エンタープライズ用のプライベートなアプリ公開や、テストアプリの有効期限長期化を切に願っています。。。よろしくお願いします。。。

#参考
- [Actions On Googleなんちゃって中級編](https://qiita.com/odetarou/items/3bb06de7276fa104236f)
- [Actions on Google SDK を用いた Action の開発で詰まった点 - Unyablog. のにれんのブログ](http://nonylene.hatenablog.jp/entry/2017/11/23/023235)
- [NodeMailer](https://nodemailer.com/about/)
- [Node.jsでメールを送信するパッケージ](https://qiita.com/yoh-nak/items/1e433b0075e3ebdef9e3)
- [nodemailerでGmailから送信するための方法](https://qiita.com/hshimo/items/ac599f5f25a3fa105ddd)