Actions on Googleプッシュ通知実装



公式のgithubにプッシュ通知のサンプルコードがありますが、daily updateとpush notificationが並列で説明されているため、今回はpush notificationの機能に絞った実装方法を紹介してみます。

また、サンプルコードの情報が少し古く、また日本語環境での実装で多少手を加える必要があるので、その部分に関しても記載したいと思います。

サンプルコード：[https://github.com/actions-on-google/dialogflow-updates-nodejs](https://github.com/actions-on-google/dialogflow-updates-nodejs)


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

まずは[Actions on Google](https://console.actions.google.com/)のコンソールに開発アカウントでログインして、今回プッシュ通知を実装するプロジェクトを作成します。

コンソールにログインしたら、「Add/import project」をクリックします。
![Actions_on_Google-01.png](https://qiita-image-store.s3.amazonaws.com/0/112736/8ea76ad0-df01-ca74-43ba-e494128aeb29.png)!


任意のプロジェクト名を入力します。今回は「pushNotificationTest」というプロジェクト名にして進めていきます。また、日本語環境でAoGを使用したいので「default language」をJapanese、「country or region」をJapanに設定します。入力が終わったら「CREATE PROJECT」をクリックします。

![Actions_on_Google-02.png](https://qiita-image-store.s3.amazonaws.com/0/112736/b542ec27-62ce-a9ae-bf59-8f4141337fdf.png)

プロジェクトのテンプレートがいろいろと用意されていますが、今回は既存テンプレートは使わないので「SKIP」をクリックして先に進みます。

![Actions_on_Google-03.png](https://qiita-image-store.s3.amazonaws.com/0/112736/108a4936-a8f5-4353-29cf-6f0d461e12af.png)


プロジェクトが作成され、プロジェクトの各種設定する画面に遷移します。まずはこのプロジェクトのAoGアプリ呼び出し名を設定するために「Decide how your Actions is invoked」をクリックします。

![Actions_on_Google-04.png](https://qiita-image-store.s3.amazonaws.com/0/112736/c89ca1ee-027d-43a4-fd18-353d565232ad.png)


Display nameの欄にAoGの呼び出し名を記入して「SAVE」をクリックします。今回は「プッシュプッシュ通知」という呼び出し名で進めます。

※他で使われている名前は「SAVE」をクリックしたあと、「他で既に使われています...」的なエラーメッセージが出るので、その場合は他の呼び出し名にする必要があります。

![Actions_on_Google-05.png](https://qiita-image-store.s3.amazonaws.com/0/112736/967f2136-4cb0-3c8d-2671-35cfe39a69d5.png)


続いて、このAoGで使用するActionsを設定するために画面左のメニュー「Actions」をクリックします。

![Actions_on_Google-06.png](https://qiita-image-store.s3.amazonaws.com/0/112736/62de1fd8-356a-4f5e-c2ed-a97742593666.png)


「ADD YOUR FIRST ACTION」をクリックしてActionの作成に入ります。

![Actions_on_Google-07.png](https://qiita-image-store.s3.amazonaws.com/0/112736/315ec2ec-508b-a798-7766-95a36481f83d.png)


ここでも各種テンプレート選択できますが、まっさらな状態のActionを制作したいので「Custom intent」を選択肢「BUILD」をクリックして進めます。

![Actions_on_Google-08.png](https://qiita-image-store.s3.amazonaws.com/0/112736/791187d8-1ca2-84ce-48a5-0187e77f55eb.png)


ここで、Actions on GoogleのコンソールからDialogflowのコンソールに遷移します。ここからはDialogflowでの設定に移るので一旦Actions on Googleの設定はそのままにしておきます。


## DialogflowでAoGのActionの作成する

日本語で使用したいので、「DEFAULT LANGUAGE」で「japanese -ja」を選択して、「DEFAULT TIMEZONE」も「Asia/Tokyo」を選択して「CREATE」をクリックします。

![Dialogflow-01.png](https://qiita-image-store.s3.amazonaws.com/0/112736/a3575648-905b-c128-1711-ffc365eb081f.png)


これで、雛形となるActions on GoogleとDialogflowの設定が完了しました。プッシュ通知を行うまでに、まだまだ他にもActions on Googleで各種設定したりDialogflowでインテントを制作および設定したり、webhookと呼ばれる実際のプログラムロジックを処理するサーバーなどを用意する必要がありますが、それぞれ今から説明していきます。


## Actions APIの設定

AoGでプッシュ通知を行う場合、Google APIsのActions APIを使用してユーザーにプッシュ通知を行います。実際には後述するfirebase cloud functions内で、このapiを使用するので、まずはActions APIの設定を済ませておきます。


Google APIsのコンソールにアクセスします。

[https://console.developers.google.com/](https://console.developers.google.com/)

※もしログインがまだであれば、今回のAoGのプロジェクトの開発アカウントでログインしてください。

### プロジェクトの選択

ウィンドウ上部の「Google APIs」の横にあるセレクターをクリックし、今回制作したプロジェクト名を選択してください。今回の説明通り作っていれば「pushNotificationTest」という名前を選択することになります。

![actions-api-01.png](https://qiita-image-store.s3.amazonaws.com/0/112736/a3327845-449e-d388-84f8-14e08a854c04.png)


プロジェクトが選択がきちんと出来ていれば「Google APIs」のロゴの横に「pushNotificationTest」と表示されているはずです。

### APIとサービスの有効化

次に、このプロジェクトでActions APIを有効にするために「APIとサービスの有効化」をクリックしてください。

![actions-api-02.png](https://qiita-image-store.s3.amazonaws.com/0/112736/f98a2dbe-f771-cd85-44e7-68adb7fbf084.png)


様々なAPIがありますが検索ウィンドウに「Actions API」と入力することで、Actions APIの設定を行うページに遷移するカードが表示されるので「Actions API」をクリックしてください。

![actions-api-03.png](https://qiita-image-store.s3.amazonaws.com/0/112736/9b4e4228-8200-b75c-d79d-1b00a7873d4d.png)


Actions APIのページが表示されるので「有効にする」をクリックします。これでこのプロジェクトでActions APIを有効化することができました。

![actions-api-04.png](https://qiita-image-store.s3.amazonaws.com/0/112736/f0c8db68-029f-c05f-10c8-627dbb588cdc.png)


### 同意画面の設定

Google APIを使用するにあたり、もう少し設定をする必要があり、まずは認証の同意画面の設定を行います。


左のメニューから「認証情報」をクリックし、タブメニューの「認証情報」を選択して「同意画面の設定」をクリックします。

![actions-api-05.png](https://qiita-image-store.s3.amazonaws.com/0/112736/d401283c-08b0-07eb-a961-9d3675f2be6d.png)


OAuth同意画面タブに移りるので、「メールアドレス」のプルダウンに今回使用している開発アカウントのメールアドレスが表示されるので、それを選択してください。

その他にも設定する項目がありますが任意のものなので適宜設定してください。

記入が終わったら画面下部の「保存」をクリックして設定を完了します。

![actions-api-06.png](https://qiita-image-store.s3.amazonaws.com/0/112736/babf1eaa-97b9-dd5e-850e-4ccdab2c5994.png)


### サービスアカウントキーの生成

次に、Actions APIを実際にapiを通じてやり取りするために、ここで生成した秘密鍵を使用して通信を行う必要があるので、その手順を説明します。

左のメニューから「認証情報」をクリックし、タブメニューの「認証情報」を選択して「認証情報を作成」のプルダウンから「サービス アカウント キー」を選択します。

![actions-api-07.png](https://qiita-image-store.s3.amazonaws.com/0/112736/073fe5e0-813f-adf0-0d32-18f1f9963e2d.png)


「サービスアカウント」のプルダウンから「新しいサービスアカウント」を選択します。

![actions-api-08.png](https://qiita-image-store.s3.amazonaws.com/0/112736/8d8e2f0e-0f10-d860-8797-eac8f5f6d569.png)


「サービス アカウント」にわかりやすい任意の名前を記入してください。今回は「pushnotificationtest-aog」とつけてみました。次に「役割」で「Project」の「オーナー」を選択します。キーのタイプでJSONにチェックを入れて「作成」をクリックしてサービスアカウントキーを生成します。

![actions-api-09.png](https://qiita-image-store.s3.amazonaws.com/0/112736/6ab39a28-8985-9fa1-48b7-95c5578612f5.png)


ダウンロードが始まり、「秘密鍵がパソコンに保存されました」と表示されます。

このサービスアカウントキーのjsonファイルは、後に実装するfirebase cloud functions内で使用するので保管しておいてください。

※このファイルはパブリックに公開しないよう注意してください。

![actions-api-10.png](https://qiita-image-store.s3.amazonaws.com/0/112736/18ce1594-6166-5cd6-b3e9-a00e5d284a60.png)



これでActions APIの設定は完了です。

ここまでで、Actions on Googleの雛形の設定、Dialogflowの雛形の設定、Actions APIの設定が完了しました。ここからはプッシュ通知を行うプログラムロジックを実装していくfirabase cloud functionsの説明に入ります。


## firebase cloud functionsの設定

今回webhookのロジック部分をfirebase cloud functionsを利用して実装していきます。

まずは、firebase consoleに開発者アカウントでログインしてください。

[https://console.firebase.google.com/](https://console.firebase.google.com/)

AoGで作成されたプロジェクトが表示されているので、該当のプロジェクトをクリックします。

![Firebase_console-01.png](https://qiita-image-store.s3.amazonaws.com/0/112736/5d304fe4-a81e-c2be-0811-f66d45f0205b.png)


左メニューの「functions」をクリックしてみると、functionは空の状態です。

これからローカルでfunctionの中身をnode.jsで記述し、firebase-toolsを使用してデプロイを行うことで、firebase cloud functionsにロジックを実装することができます。

![Firebase_console-02.png](https://qiita-image-store.s3.amazonaws.com/0/112736/5df12938-96d7-2a16-3dea-d6e4caf22f42.png)



### firebase-toolsのインストール

firebase cloud functionsのロジックを開発するにあたり、今回はnode.jsを使用します。

※事前にローカルの開発マシンにnode.jsのv8以上をインストールしておいてください。（node.jsのインストールについては割愛させていただきます。）

まずは`npm`を利用して`firebase-tools`をグローバルにインストールします。

```sh:terminal
$ npm install -g firebase-tools
```

firebase-toolsをインストールすることによって、`firebase`コマンドを利用することができるようになります。

`firebase`コマンドを利用して、今回作成したfirebaseプロジェクトのfunctionsにコードをデプロイしていきます。

### コマンドラインからfirebaseにログイン

まずはターミナルを開き、コマンドラインから開発アカウントでfirebaseにログインします。

```sh:terminal
$ firebase login
? Allow Firebase to collect anonymous CLI usage and error reporting information?
 (Y/n) 
```

`Y`をクリックするとブラウザにFirebase CLIで使用するアカウントが聞かれるので、今回のプロジェクトに紐付いたアカウントを選択して「許可」する。`✔  Success! Logged in as <アカウント>`が出ればOK

### firebaseのプロジェクトを確認

```sh:terminal
$ firebase list
```

これでアカウントに紐付いたプロジェクトが一覧できる、今回でいうと`pushNotificationTest`が表示されていれば、正しいfirebaseのアカウントでログインできている。

※ここでプロジェクトが表示されない場合は、別アカウントでfirebaseにログインしている可能性があるので、その際は`firebase logout`して再度`firebase login`をして正しいアカウントでログインし直す。

### プロジェクトディレクトリの作成

firebase cloud functionsにデプロイするコードを記述していくプロジェクトディレクトリをローカルに作成します。

※プロジェクトディレクトリの名前は任意。説明では`pushNotificationTest`というディレクトリ名で進めます。

```sh:terminal
$ mkdir pushNotificationTest
$ cd pushNotificationTest
```

### firebaseの初期化

プロジェクトディレクトリを制作したので、`firebase init`コマンドで、firebaseの初期化をまずは行います。ここでアカウントに紐付いたfirebaseの各種設定ファイルなどを生成し、該当のfirebaseプロジェクトに適切なデプロイを行うことができるようになります。

`firebase init`コマンドを実行し、`Functions`までカーソルを移動して「スペースキー」を押して有効にして「Enter」。

```sh:terminal
$ firebase init
? Which Firebase CLI features do you want to setup for this folder? Press Space 
to select features, then Enter to confirm your choices. 
 ◯ Database: Deploy Firebase Realtime Database Rules
 ◯ Firestore: Deploy rules and create indexes for Firestore
❯◉ Functions: Configure and deploy Cloud Functions
 ◯ Hosting: Configure and deploy Firebase Hosting sites
 ◯ Storage: Deploy Cloud Storage security rules
```

該当のプロジェクトまでカーソルを移動して「Enter」。

```sh:terminal

? Select a default Firebase project for this directory: 
  [don't setup a default project] 
❯ pushNotificationTest (***********) 
  [create a new project] 
```

今回はJavaScriptを選択（TypeScriptで書きたい方はTypeScript）


```sh:terminal

? What language would you like to use to write Cloud Functions? (Use arrow keys)

❯ JavaScript 
  TypeScript 
```

ESLintを使用するかどうかを聞かれるので、こちらはお好みで選択してください。

```sh:terminal
? Do you want to use ESLint to catch probable bugs and enforce style? (y/N) 
```

`npm install`をこの時点で実行するか？と聞かれます。あとでnpmのモジュールを追加しますが、まずは`Y`を選べば良いと思います。

`functions`ディレクトリ下に`node_modules`が作成され、生成された`package.json`を参照して、該当のモジュールのインストールが開始されます。

```sh:terminal
? Do you want to install dependencies with npm now? (Y/n) 
```

`✔  Firebase initialization complete!`と出れば完了。

これでfirebaseの初期化は終了です。
次にwebhookのロジックの起点となる`functions/index.js`にAoGのwebhookに必要となるコーディングを行っていきます。

## firebase functionsにデプロイ

firebase functionsにまだデプロイしていないため、Dialogflowの`Fullfillment`の`Webhook`アドレスが決まっていないので、最小限のコードを記述して`firebase deploy`を行います。

### actions-on-googleのnode_modulesをインストール

まずはAoGやDialogflowの通信用に用意された`actions-on-google`のnode.js SDKをインストールします。

`firebase init`で制作された`functions`ディレクトリに移動して`actions-on-google`をインストール

```sh:terminal
$ cd functions
$ npm install --save actions-on-google
```

### firebase functionsのnodeをv8にするための設定

現時点ではデフォルトのfirebaseの実行環境がnode.jsのv6系ですが、今後のこともふまえてv8系で実行できるように設定しておきます。

`functions/package.json`に`"engines": {"node": "8"}`のコードを追記指定することで、firebase functionsのnode.js実行環境をv8で実行するように設定できます。

```json:package.json
{
  "name": "functions",
  "description": "Cloud Functions for Firebase",
  "engines": {
    "node": "8"
  },
  "scripts": {
    "serve": "firebase serve --only functions",
    "shell": "firebase functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "dependencies": {
    "actions-on-google": "^2.3.0",
    "firebase-admin": "~6.0.0",
    "firebase-functions": "^2.0.3"
  },
  "private": true
}
```

### index.jsを記述とデプロイ

まずはfirebase cloud functionsのURLを発行し、Dialogflowのwebhookに設定するために`Default Welcome Intent`のみに対応した最小限のコードを記載してデプロイしてみます。

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

const {
    dialogflow
} = require('actions-on-google');
const functions = require('firebase-functions');

const app = dialogflow({ debug: true });

app.intent('Default Welcome Intent', (conv) => {
    conv.close('プッシュプッシュ通知')
});

exports.pushNotificationTestFullfillment = functions.https.onRequest(app);
```

### firebase functionsのURLをコンソールから確認

デプロイするとfirebase consoleのFunctionsで「トリガー」のURLが記載されているので、そのURLをコピーします。

![Firebase_console-03.png](https://qiita-image-store.s3.amazonaws.com/0/112736/0ba9a5fd-0259-fa03-64c6-b7f1bf522786.png)


### DialogflowでFullfillmentにwebhookを設定

Dialogflowのコンソールの`Fullfillment`から`Webhook`の`ENABLED`トグルをオンにして、先程コピーしたURLを`URL`の欄にペーストし、画面下部までスクロールして「SAVE」ボタンを押す。

![Dialogflow-02.png](https://qiita-image-store.s3.amazonaws.com/0/112736/526e6e0b-d2c1-76e5-6a23-aa83a214d2c7.png)


### 該当intentでwebhookを使用する設定

`Default Welcome Intent`でfirebase cloud functionsでロジックを処理できるように`Fullfillment`の項目で`Webhook`を使うように設定します。

![Dialogflow-03.png](https://qiita-image-store.s3.amazonaws.com/0/112736/7f6fa77b-6481-9c7b-7d22-dabe39e1ed50.png)


`Default Welcome Intent`の画面下部までスクロールして`Fullfillment`の`Enable webhook call for this intent`のトグルをオンにして「SAVE」をクリック

![Dialogflow-04.png](https://qiita-image-store.s3.amazonaws.com/0/112736/292ca505-ef03-d762-ab73-bd9552525049.png)


### シュミレータでのチェック

先程デプロイした「プッシュプッシュ通知」と帰ってきていればOK

![Actions_on_Google-09.png](https://qiita-image-store.s3.amazonaws.com/0/112736/8db4597e-96d6-a7ee-225f-14327f442b56.png)



## firebase firestoreの設定

今回のサンプルコードでは`firestore`のデータを参照してティップスを出しているので、まずはfirestoreにデータを格納していきます。

直にfirebaseコンソールからfirestoreにデータを手作業で入れても問題ないですが、サンプルコードでは初期のデータをfirestoreに格納できるコードが記載されているので、そちらを利用してデータを入れ込んでいきます。

### firestoreにデータを格納

#### サンプルソースを参考にfirestoreにデータを格納するfunctionを記述

```js:index.js

// 省略

const admin = require('firebase-admin');

/** Collections and fields names in Firestore */
const FirestoreNames = {
    CATEGORY: 'category',
    CREATED_AT: 'created_at',
    INTENT: 'intent',
    TIP: 'tip',
    TIPS: 'tips',
    URL: 'url',
    USERS: 'users',
    USER_ID: 'userId',
};

admin.initializeApp();
const db = admin.firestore();
db.settings({ timestampsInSnapshots: true }); // dateの扱いが変更されたようでエラーを回避するためこの行を追加（これを記載していないとfirebase functionsのログコンソールでエラーが出て、正常に動作しない可能性がある）

// 省略

// Use this function to restore the content of the tips database.
exports.restoreTipsDB = functions.https.onRequest((request, response) => {
    db.collection(FirestoreNames.TIPS)
        .get()
        .then((querySnapshot) => {
            if (querySnapshot.size > 0) {
                let batch = db.batch();
                querySnapshot.forEach((doc) => {
                    batch.delete(doc.ref);
                });
                batch.commit()
                    .then(addTips);
            }
        })
        .catch((error) => {
            throw new Error(`Firestore query error: ${error}`);
        });
    addTips();

    /**
     * Add tips
     */
    function addTips() {
        const tips = require('./tipsDB.json');
        let batch = db.batch();
        let tipsRef = db.collection(FirestoreNames.TIPS);
        tips.forEach((tip) => {
            let tipRef = tipsRef.doc();
            batch.set(tipRef, tip);
        });
        batch.commit()
            .then(() => {
                response.send(`Tips DB succesfully restored`);
            })
            .catch((error) => {
                throw new Error(`Error restoring tips DB: ${error}`);
            });
    }
});
```

firestoreに格納するデータテンプレートをtipsDB.jsonとしてindex.jsと同階層に作成します。

こちらがfirestoreに入れ込むデータのテンプレートファイルになります。今回はサンプルコードに書かれているものをそのまま使用していますが、tipの文言などが英語のままになっているのでお好みで日本語に書き直すなどしてみるとよりわかりやすいかもしれません。


```json:tipsDB.json
[{
    "category": "tools",
    "url": "https:\/\/www.youtube.com\/watch?v=sjpbu4y6F_M&list=PLOU2XLYxmsILvfJcIASBDbgfxloFz_XsU&index=5",
    "tip": "If you don't know how to code, you can use templates to build an App! Learn more about it in our Build an App for the Google Assistant with Templates and No Code video.",
    "created_at": "2017-12-25T08:00:00.000Z"
},
{
    "category": "voice user interface",
    "url": "https:\/\/developers.google.com\/actions\/assistant\/best-practices",
    "created_at": "2017-12-21T08:00:00.000Z",
    "tip": "Remember that your users may need your app to repeat itself at any time, so keep track of your last prompt and be prepared to repeat it."
},
{
    "category": "tools",
    "url": "https:\/\/github.com\/actions-on-google\/actions-on-google-nodejs",
    "tip": "The Actions On Google client library for Dialogflow and Actions SDK is a nodejs module that helps you build your fulfillment whether you're using Dialogflow or the Actions SDK. ",
    "created_at": "2017-12-19T08:00:00.000Z"
},
{
    "category": "voice user interface",
    "url": "https:\/\/developers.google.com\/actions\/reference\/ssml",
    "tip": "Use SSML, or speech synthesis markup language, to improve the quality of your app with features like custom audio and granular speech synthesis controls.",
    "created_at": "2017-12-17T08:00:00.000Z"
},
{
    "category": "voice user interface",
    "url": "https:\/\/www.youtube.com\/watch?v=cHEahGHseGc&list=PLOU2XLYxmsILvfJcIASBDbgfxloFz_XsU&t=1s&index=4",
    "tip": "When designing a voice user interface, you should avoid simply converting a graphical user interface to a voice user interface. Learn more tips about designing VUI watching our \"How to better design VUI for the Google Assistant\" video.",
    "created_at": "2017-12-13T08:00:00.000Z"
},
{
    "category": "dialogflow",
    "url": "https:\/\/developers.google.com\/actions\/dialogflow\/define-actions#define_additional_actions",
    "tip": "If using Dialogflow, you can specify additional actions for your app as additional triggering intents in the Assistant Integrations page. The user says phrases of those triggering intents will also be used for app discovery.",
    "created_at": "2017-12-11T08:00:00.000Z"
},
{
    "category": "development",
    "url": "https:\/\/developers.google.com\/actions\/reference\/rest\/Shared.Types\/AppRequest#Conversation",
    "tip": "Use the data property of our client library interface to persist data within a dialog. It uses the conversation Token field of the conversation API request and response. ",
    "created_at": "2017-12-09T08:00:00.000Z"
},
{
    "category": "development",
    "url": "https:\/\/www.youtube.com\/watch?v=GKP4AS4L_K8&list=PLOU2XLYxmsILvfJcIASBDbgfxloFz_XsU&t=1s&index=3",
    "tip": "The most common cause for app rejection during review is incomplete store listing data. Learn more tips about passing the review in our How to Publish an App for the Google Assistant that Will Pass Review video.",
    "created_at": "2017-12-08T08:00:00.000Z"
},
{
    "category": "development",
    "url": "https:\/\/developers.google.com\/actions\/assistant\/best-practices",
    "tip": "Use the last Seen property of the incoming request to determine return users, and greet them differently than new users.",
    "created_at": "2017-12-07T08:00:00.000Z"
},
{
    "category": "dialogflow",
    "url": "https:\/\/developers.google.com\/actions\/assistant\/best-practices",
    "tip": "If using Dialogflow, provide at least 10-12 examples of User Says phrases for your intents.",
    "created_at": "2017-12-06T08:00:00.000Z"
},
{
    "category": "development",
    "url": "https:\/\/developers.googleblog.com\/2017\/11\/actions-on-google-best-practices-video.html",
    "tip": "You can learn all the best practices for Actions on Google watching videos from the Actions On Google Best Practices Video Series.",
    "created_at": "2017-12-05T08:00:00.000Z"
},
{
    "category": "development",
    "url": "https:\/\/developers.google.com\/actions\/discovery\/implicit#writing_useful_action_invocation_phrases",
    "tip": "When defining action phrases, try to make sure they have a verb and an object to improve the chances for discovery.",
    "created_at": "2017-12-04T08:00:00.000Z"
},
{
    "category": "voice user interface",
    "url": "https:\/\/developers.google.com\/actions\/assistant\/best-practices",
    "tip": "Be prepared to help whenever your user asks for it, and when they do, give them some foothold into the conversation.",
    "created_at": "2017-12-03T08:00:00.000Z"
},
{
    "category": "voice user interface",
    "url": "https:\/\/developers.google.com\/actions\/assistant\/best-practices",
    "tip": "If a user provides some unrecognized input, first try a simple rapid reprompt like \"What was that?\" to keep the conversation moving quickly. As they continue to provide unrecognized input, expand the reprompting to be more clear until finally ending the conversation after 3 failures.",
    "created_at": "2017-12-01T08:00:00.000Z"
},
{
    "category": "tools",
    "url": "https:\/\/developers.google.com\/actions\/tools\/sound-library\/",
    "tip": "Use our sound library to insert custom audio through SSML. This makes your voice experience far more engaging!",
    "created_at": "2017-12-02T08:00:00.000Z"
}]
```

firestoreにデータ格納する準備が整ったので、修正したものをfirebaseにデプロイします。

```sh:terminal
$ firebase deploy --only functions
```

`deploying functions`以下が表示されて新しく追加した`restoreTipDB`ファンクションが登録されたのがわかります。

```sh:terminal
Function URL (restoreTipsDB): https://us-central1-<project_id>.cloudfunctions.net/restoreTipsDB
```

### firebase コンソールでfirestoreの設定

firestoreのデータ格納のコードは準備できましたが、まだfirestoreの設定が済んでいないのでfirebase consoleからfirestoreの初期設定を行っていきます。

firebaseコンソールを開いて、左のメニューから`database`を選択し、Cloud Firestore「データベースを作成」をクリックします。

![Firebase_console-04.png](https://qiita-image-store.s3.amazonaws.com/0/112736/1fb75a33-4c83-7722-f943-b03bf551e37b.png)


#### firestoreのアクセス権限の設定

firestoreのアクセス権限は、今回テスト目的なので、テストモードで開始を選択して「有効にする」をクリックします。

※本番ではきちんとセキュリティー設定をすることを強くおすすめします。

![Firebase_console-05.png](https://qiita-image-store.s3.amazonaws.com/0/112736/24ed1d1d-6f6d-fc07-2449-457ebdd113e5.png)


firestoreの設定ができているかを実際に`Database`を選択して確認してみます。

![Firebase_console-06.png](https://qiita-image-store.s3.amazonaws.com/0/112736/440f7da4-0c13-02fb-b904-bcc962e8c6ee.png)


#### firestoreに初期データを格納

最初はデータが空の状態なので、先程デプロイした`restoreTipDB`にアクセスしてDBにデータを格納してみます。

先程デプロイ時に表示されたFunction URL (restoreTipsDB)の`https://us-central1-<project_id>.cloudfunctions.net/restoreTipsDB`URLを、ブラウザを開いてアスセスすることで、restoreTipDBを実行し、firestoreにデータを作成することができます。

無事制作できるとブラウザに`Tips DB succesfully restored`と表示されます。

#### firesotreにデータが格納されたかを確認

firebaseコンソールの`Database`を開くと、先程空だったものにデータが作成できたことを確認できます。

![Firebase_console-07.png](https://qiita-image-store.s3.amazonaws.com/0/112736/4839fd2d-9c32-2711-e502-71ae76c257fe.png)


これで、firestoreの設定および初期データの格納ができました。次は実際にfirestoreのデータを取得してAoGに表示するコードを実装していきます。


## firestoreからデータを取得してみる

index.jsの`Default Welcome Intent`にアクセスした時にfirestoreに格納されたデータを取得・出力するコードを追記および修正します。

### index.jsの修正

※サンプルコードを参考に実装

```js:index.js

// 省略

const {
    dialogflow,
    Suggestions
} = require('actions-on-google');

// 省略

/** App strings */
const RANDOM_CATEGORY = 'ランダム';
const RECENT_TIP = '最新情報';

// 省略

app.intent('Default Welcome Intent', (conv) => {
    // get available categories to show in the welcome message and in the
    // suggestion chips.
    return db.collection(FirestoreNames.TIPS)
        .get()
        .then((querySnapshot) => {
            // create an array that contains only the unique values of categories
            const uniqueCategories = querySnapshot.docs.map((currentValue) => {
                    return currentValue.get(FirestoreNames.CATEGORY);
                })
                .filter((element, index, array) => {
                    return array.indexOf(element) === index;
                });
            uniqueCategories.unshift(RECENT_TIP);
            const welcomeMessage = `グーグル ティップスへようこそ。 ` +
                `アクションズ オン グーグルのティップスを選択することができます。` +
                `最新に追加されたティップスか、以下の項目のカテゴリーから選択できます。` +
                `${uniqueCategories.join(', ')}。` +
                `もしくはランダムにティップスを提供します。`;
            const screenOutput =
                conv.surface.capabilities.has('actions.capability.SCREEN_OUTPUT');
            if (!screenOutput) {
                return conv.ask(welcomeMessage);
            }
            uniqueCategories.push(RANDOM_CATEGORY);
            conv.ask(welcomeMessage);
            conv.ask(new Suggestions(uniqueCategories));
        });
})

```

### 修正したコードをデプロイ

最新情報やランダムの入力に対しての受けのIntentはまだ作成していないですが、`Default Welcome Intent`にアクセスした時にfirestoreからデータを取得して表示させる部分をデプロイして確かめてみます。

```sh:terminal
$ firebase deploy --only functions
```

### シミュレータでの確認

AoGのコンソールからシミュレーターを起動して動作しているか確認してみます。

![](./README_img/Actions_on_Google-10.png)

無事、firestoreに格納したデータが表示されていればOKです。


## ユーザーの「最新情報」の発話に答えるインテントを作成

最新情報のユーザー発話に対応したインテントを作成します。

### Dialogflowでインテントを作成

Dialogflowコンソール画面から`Intents`の`CREATE INTENT`をクリックします。

![Dialogflow-05.png](https://qiita-image-store.s3.amazonaws.com/0/112736/f4d2252c-0b38-ca61-a9e4-4995824f60d2.png)


サンプルコードに習って`tell_latest_tip`という名前のインテントにします。`Training phrases`に「最新情報」を追加して「最新情報」というユーザー発話をフックできるようにしておきます。画面をスクロールダウンして`Fullfillment`の`Enable webhook call for this intent`のトグルをオンにして「SAVE」をクリックします。

![Dialogflow-06.png](https://qiita-image-store.s3.amazonaws.com/0/112736/1ec22bad-2cc9-59c8-8536-a4056a897c44.png)


### firebase functionsのindex.jsにtell\_latest\_tipを受け付けるインテント処理を実装

Dialogflow側でインテントを設定したので、今度はwebhookのロジック側でそのインテントを受け付けた時の処理を行うコードをindex.jsに実装していきます。

```js:index.js

// 省略

const {
    dialogflow,
    Suggestions,
    BasicCard,
    Button
} = require('actions-on-google');

// 省略

app.intent('tell_latest_tip', (conv) => {
    return db.collection(FirestoreNames.TIPS)
        .orderBy(FirestoreNames.CREATED_AT, 'desc')
        .limit(1)
        .get()
        .then((querySnapshot) => {
            const tip = querySnapshot.docs[0];
            const screenOutput =
                conv.surface.capabilities.has('actions.capability.SCREEN_OUTPUT');
            if (!screenOutput) {
                return conv.close(tip.get(FirestoreNames.TIP));
            }
            conv.ask(tip.get(FirestoreNames.TIP));
            conv.ask(new BasicCard({
                text: tip.get(FirestoreNames.TIP),
                buttons: new Button({
                    title: 'Learn More!',
                    url: tip.get(FirestoreNames.URL),
                }),
            }));
        });
});

```

### 修正コードをデプロイ

修正したコードをデプロイします。

```sh:terminal
$ firebase deploy --only functions
```

### シミュレータでの確認

AoGのシミュレーターを起動してfirestoreから取得した最新情報が表示されていることが確認できます。

![Actions_on_Google-11.png](https://qiita-image-store.s3.amazonaws.com/0/112736/b9291728-bdb0-7579-2af7-58c90f12e3cd.png)


長かったですが、これでプッシュ通知を行うコード実装の準備が整いました。次に実際にプッシュ通知の実装について記述していきます。

## プッシュ通知の実装

ここからが本題のプッシュ通知を送る部分の説明になります。

手順としては

1. UpdatePermissionを受け付けるインテントを作成して、ユーザーに通知を受けるかどうかを有効にするかを聞きます。この時、同時にプッシュ通知時に起動するインテントを設定します（※今回は最新情報を出す`tell_latest_tip`になります）。
2. 事前に`tell_latest_tip`インテントが呼び出されるようにDialogflowコンソールおよびActions on Googleコンソールで`User engagement`の設定を行います。
3. ユーザーが通知を受け取ることに「はい」つまり承認した時に受け付けるインテントの設定・実装します。この時userIDを取得し、それをfirestoreに格納しておくことで通知すべきuserIDがわかるようにします。※後述する`conv.arguments.get('UPSATES_USER_ID')`は一度しか取得できないので、もしfirestoreに`userID`に保存の失敗した場合は、別途userIDを取得し直すコード・及びロジックを書かないといけないので要注意）
4. firestoreに新しいデータが作られた時に、プッシュ通知を送る機能を先に説明したgoogleapiのActions APIを使って実装します。


### 1.UpdatePermissionを受け付けるインテントの作成とindex.jsの追加修正

#### Dialogflowでインテントの作成

Dialogflowのコンソールの左メニュー`Intents`を選択し、「CREATE INTENT」をクリックします。

![Dialogflow-10.png](https://qiita-image-store.s3.amazonaws.com/0/112736/8d3fc972-7b4c-f3ca-0eb0-eb25692b328c.png)


インテント名をサンプロコードにならって`setup_push`とし、「新しいティップスを通知して」とユーザーが発話した時に呼ばれるように設定します。画面をスクロールダウンして`Fullfillment`の`Enable webhook call for this intent`のトグルをオンにして「SAVE」をクリックします。

![Dialogflow-11.png](https://qiita-image-store.s3.amazonaws.com/0/112736/be317356-9b5d-b87b-f544-6b754ead47e3.png)


#### index.jsのコード修正・追加

Dialogflowの`setup_push`のインテントを受け付けるためのコードをindex.jsに追加修正します。

ユーザーが「新しいティップスを通知して」と発話すると、`setup_push`インテントが呼ばれ、index.jsの`setup_push`インテント処理ないで`UpdatePermission()`を呼ぶようにしています。

※`UpdatePermission()`を呼ぶことでAoGのEventの`actions_intent_PERMISSION`が呼ばれます。後述しますが、この`actions_intent_PERMISSION`Eventを受けるインテントを設定していない状態ではエラーになるので要注意です。

```js:index.js
// 省略

const {
    dialogflow,
    BasicCard,
    Button,
    Suggestions,
    UpdatePermission
} = require('actions-on-google');

// 省略

/** App strings */
const TELL_LATEST_TIP_INTENT = 'tell_latest_tip';

// 省略

// Start opt-in flow for push notifications
app.intent('setup_push', (conv) => {
    conv.ask(new UpdatePermission({ intent: TELL_LATEST_TIP_INTENT }));
});
```

### 2.プッシュ通知時に呼び出されるインテントの設定

プッシュ通知を届けた時に呼び出すインテントの設定を行います。AoGコンソールでインテントにユーザーに通知を受ける許可を受られる設定を行います。

#### Dialogflowのintegrationを設定

DialogflowのintegrationからGoogle Assistantの「INTEGRATION SETTINGS」をクリックします。

![Dialogflow-07.png](https://qiita-image-store.s3.amazonaws.com/0/112736/35df19cc-3c0a-713a-5acb-fc85941db2ad.png)


#### AoGで呼び出しインテントをActionsに追加

インテント設定のUser engagementの設定をすできるようにするためにDiscoveryのImplicit invocationに通知時に呼び出されるインテントを選択します。今回の場合は`tell_latest_tip`になります。

※Implicit invocationは暗黙的呼び出しの設定でもあるので、少々気になりますが、現状のUIではこうすることでしかインテントの`User engagement`を設定できないのでここで設定しています。

![Dialogflow-08.png](https://qiita-image-store.s3.amazonaws.com/0/112736/2aa1cfcd-061f-dfe5-0963-05a8b03a720f.png)


「TEST」をクリックして、Actions on Googleでの設定に移ります。

![Dialogflow-09.png](https://qiita-image-store.s3.amazonaws.com/0/112736/6c05e4f5-09a2-5feb-868c-37a4a6843e1f.png)


#### Actions on GoogleのコンソールでActionsの設定

Actions on Googleのコンソールの左メニュー「Actions」を選択すると、先程Dialogflowで設定したインテント（今回の場合`tell_latest_tip`）が表示されているのでクリックします。

![Actions_on_Google-12.png](https://qiita-image-store.s3.amazonaws.com/0/112736/26756ed2-0863-8d52-d530-c29458c70ed9.png)


#### User engagementの設定

画面をスクロールダウンして`User engagement`の項目の`Would you like to send push notifications? If yse, user permission will be needed`のトグルをオンにします。

`Content title`にユーザーにパーミッションを求める時の文言を設定しておきます。

![Actions_on_Google-13.png](https://qiita-image-store.s3.amazonaws.com/0/112736/03e985ce-6840-5232-66fe-6e70124847a8.png)


### 3.UpdatePermissionを受けつけるインテントの作成とindex.jsの追加修正

「1.UpdatePermissionを受け付けるインテントの作成...」で作成した`UpdatePermission`を呼び出した時に、Eventの`actions_intent_RERMISSION`が発生するので、それを受け取るためのインテントを作成します。

Dialogflowのコンソールの左メニューのintentsを選択し「CREATE INTENT」をクリックします。

![Dialogflow-12.png](https://qiita-image-store.s3.amazonaws.com/0/112736/62f4a61a-cf63-f0e3-be2f-8cf41fd9da95.png)


インテント名を設定し（ここではサンプルコードにならって`finish_push_setup`としています）、Eventsに`actions_intent_PERMISSION`と記入します。

画面をスクロールダウンして`Fullfillment`の`Enable webhook call for this intent`のトグルをオンにして「SAVE」をクリックします。

![Dialogflow-13.png](https://qiita-image-store.s3.amazonaws.com/0/112736/308641db-a716-95ca-a256-f6d80dc0d54d.png)


#### index.jsのコードを追加修正

ユーザーに許可を求める返事の「はい」「いいえ」を`conv.arguments.get('PERMISSION')`で取得することができます。

「はい」のときは`conv.arguments.get('UPDATES_USER_ID')`からユーザーIDを取得して、firestoreにプッシュ通知で起動するインテント名とユーザーIDを格納します。

「いいえ」のときは、ユーザーに通知しない旨を伝えて、そのままセッションを切断しています。

**※注意事項** `conv.arguments.get('UPDATES_USER_ID')`は`conv.arguments.get('PERMISSION')`trueの初回時のみ取得できるようで、二回目以降は`undefined`を返してしまうのでfirestoreなどに格納ミスを行うと、ユーザーIDが取得できなくなってしまうので、別途匿名ユーザーのIDの取得する方法などで取得し直すコードおよびロジックを組む必要があります。←ここハマるところです。

```js:index.js
// 省略

// Save intent and user id if user gave consent.
app.intent('finish_push_setup', (conv, params) => {
    if (conv.arguments.get('PERMISSION')) {
    	 //以下のcov.arguments.get('UPDATES_USER_ID')はPERMISSIONを承認した最初にしか取れないので、firestoreに保存失敗したときは匿名ユーザーIDの取得方法などで実装する必要がある
        const userID = conv.arguments.get('UPDATES_USER_ID');
        
        return db.collection(FirestoreNames.USERS)
            .add({
                [FirestoreNames.INTENT]: TELL_LATEST_TIP_INTENT,
                [FirestoreNames.USER_ID]: userID,
            })
            .then((docRef) => {
                conv.close(`通知を開始します。`);
            });
    } else {
        conv.close(`通知はしません。`);
    }
});

// 省略

```

#### index.jsをデプロイ

では修正したコードをデプロイします。

```sh:terminal
$ firebase deploy --only functions
```

#### シミュレーターで実行して確認

![Actions_on_Google-14.png](https://qiita-image-store.s3.amazonaws.com/0/112736/a2825748-00cd-d1f9-0714-33c4100b002d.png)


#### firestoreに保存されているかを確認

![Firebase_console-08.png](https://qiita-image-store.s3.amazonaws.com/0/112736/07c9b1c1-cead-9b60-e837-06e03542f5a0.png)


これでプッシュ通知を受け付けるAoG、Dialogflow、firebase cloud functions（index.js)、firestoreの準備が完了しました。

あとは、実際にActions APIを通じてプッシュ通知を送るトリガーを実装することでプッシュ通知を受け取ることができます。

では最後にサンプルコードにならってプッシュ通知を送る部分を実装していきます。

### 4. firestoreに新しいデータが作られた時に、プッシュ通知を送る機能をgoogleapiのActions APIを使って実装

firestore内のtipsデータに新しくドキュメントが追加されたことを検知するファンクションを用意して、そこからgoogleapiを使用しActions API経由でユーザーにプッシュ通知を送ります。

#### 新しいファンクションで使用する`googleapi`と`request`をnpmでインストール

```sh:terminal
$ npm install --save googleapis
$ npm install --save request
```

前述した、Actions APIで生成したサービスアカウントの秘密鍵jsonファイルをわかりやすいように`service-account-actionsapi.json`などにリネームして`index.js`と同階層に移動しておきます。

#### index.jsの編集

**注意事項：** サンプルコードのままだと、エラーで動かないので以下の二点に注意する必要があります。


1. `const google = require('googleapis')`とサンプルコードでは記載されているが、googleapisの仕様変更により記法が変わったので`const {google} = require('googleapis')`とする
2. `notification.target = {}`内に`locale: 'js'`の追記をする

```js:index.js

exports.createTip = functions.firestore
    .document(`${FirestoreNames.TIPS}/{tipId}`)
    .onCreate((snap, context) => {
        const request = require('request');
        const {google} = require('googleapis');
        const serviceAccount = require('./service-account-actionsapi.json');
        const jwtClient = new google.auth.JWT(
            serviceAccount.client_email, null, serviceAccount.private_key, ['https://www.googleapis.com/auth/actions.fulfillment.conversation'],
            null
        );
        let notification = {
            userNotification: {
                title: '最新のティップスが追加されました！',
            },
            target: {},
        };
        jwtClient.authorize((err, tokens) => {
            if (err) {
                throw new Error(`Auth error: ${err}`);
            }
            return db.collection(FirestoreNames.USERS)
                .where(FirestoreNames.INTENT, '==', TELL_LATEST_TIP_INTENT)
                .get()
                .then((querySnapshot) => {
                    querySnapshot.forEach((user) => {
                        notification.target = {
                            userId: user.get(FirestoreNames.USER_ID),
                            intent: user.get(FirestoreNames.INTENT),
                            locale: 'ja',
                        };
                        request.post('https://actions.googleapis.com/v2/conversations:send', {
                            'auth': {
                                'bearer': tokens.access_token,
                            },
                            'json': true,
                            'body': { 'customPushMessage': notification, 'isInSandbox': true },
                        }, (err, httpResponse, body) => {
                            if (err) {
                                throw new Error(`API request error: ${err}`);
                            }
                            console.log(`${httpResponse.statusCode}: ` +
                                `${httpResponse.statusMessage}`);
                            console.log(JSON.stringify(body));
                        });
                    });
                })
                .catch((error) => {
                    throw new Error(`Firestore query error: ${error}`);
                });
        });
        return 0;
    });
```

#### index.jsをデプロイ

では修正したコードをデプロイします。

```sh:terminal
$ firebase deploy --only functions
```

## プッシュ通知の送信

実際はプログラムによってfirestoreに新しいデータを生成すると思いますが、今回はfirestoreのwebコンソールに直接データを作成して、プッシュ通知が届けてみます。

tipsの「ドキュメント追加」をクリックします。

![Firebase_console-09.png](https://qiita-image-store.s3.amazonaws.com/0/112736/b25ea376-a4f8-52c6-254d-422190d077d2.png)


必要なフィールドと値を記入して「保存」を押します。

![Firebase_console-10.png](https://qiita-image-store.s3.amazonaws.com/0/112736/0bc79e9b-6911-373d-4026-b37a43a328fe.png)

多少のタイムラグが有るときが結構ありますが、プロジェクトを制作したアカウントでモバイルのGoogle Assistantにログインしていれば、Google Assistantに通知が届きます。

## 要検証

- プッシュ通知を削除する方法が不明（※1）
- プッシュ通知をユーザー全員に送る仕様になっているので、出し分けロジックをどうするか？
- プッシュ通知をユーザー全員にfor文でわまして送信しているので、数千人規模になった時googleapiの料金・及びパフォーマンスがどうなるか？

※1：公式リファレンスの[User Engagement](https://developers.google.com/actions/assistant/updates/overview)のManage updatesの項目や、[Google Actions: Stop sending updates from “app_name” issue](https://stackoverflow.com/questions/49535629/google-actions-stop-sending-updates-from-app-name-issue)で英語での解説があるが`stop sending updates from <action_name>`を日本語でどういうか？が不明