kun432's blog

Alexaなどスマートスピーカーの話題中心に、Voiceflowの日本語情報を発信してます。たまにAWSやkubernetesなど。

〜スマートスピーカーやVoiceflowの記事は右メニューのカテゴリからどうぞ。〜

Voiceflow TIPS #35 Firebase RealtimeDatabaseでデータを管理する

皆さん、スキル内課金対応スキル作っていますか?スキルチャレンジもあるし、Echo Show5がもらえる公式キャンペーンもあるし、で、やるしかないですよねー。ということで、現在スキル内課金対応スキルを作成している今日このごろです。

で、話は変わって、Advent Calendar延長戦として、今回はVoiceflowでFirebaseと連携させてみようと思います。

Firebaseとは?

最初にFirebaseについてです。このあたりを見ればよいのではないかと思います。

簡単にまとめると、

  • Google提供の、モバイルおよび Web アプリケーションのバックエンドサービス
  • 以下を提供してくれる
    • データベース
    • 認証機能
    • ストレージ
    • メッセージング
    • サーバレスの実行環境
  • これらを活用することで、アプリケーションの開発に専念できる

というものですね。とても便利そうです。

なぜVoiceflowでFirebase?

メリットは上に書いたとおりですが、なぜVoiceflowで使うのか?ということです。今回は特にデータベースの部分に注目してみたいと思います。

Voiceflowでデータの管理を行う方法は、標準だと以下になるかと思います。

どちらも気軽に使えるので便利なのですが、それぞれデメリットもあります。

  • Project Variablesを使って永続データとして管理
    • 変数はあくまでもスキル内からしか利用できない。スキル開発者が管理することができない。
    • 集計などの横断的なアクセスができない。
  • Googleスプレッドシートをデータベース的に使う
    • アクセスが遅く、エラーが出たりなど、イマイチ信頼性が低い
    • Integration Blockの設定が面倒かつあまり融通が利かない

そこで、Firebaseの出番です。今回はVoiceflowのフォーラムにある、以下のチュートリアルを参考に、ユーザデータの管理をFirebase Realtime Databaseで試してみたいと思います。

管理するデータ

ユーザIDは、Voiceflowのuser_id変数で取得できますので、これをキーに以下の情報を紐付けて、Firebaseに保存するようにしたいと思います。

  • スキルの起動回数
  • スキル実行開始・終了の日時
  • スキルの実行時間
  • バイスに画面があるか?
  • ユーザのタイムゾーン

プロジェクトの作成

https://firebase.google.com/ にアクセスして、右上の「コンソールへ移動」をクリックします。

f:id:kun432:20191015185438p:plain

「プロジェクトを追加」をクリックして、新しいプロジェクトを作成します。

f:id:kun432:20191015185448p:plain

プロジェクト名を入力します。ここでは"firebase-voiceflow-sample"としました。「続行」をクリックします。

f:id:kun432:20191015190037p:plain

プロジェクトの解析にGoogleアナリティクスを使用するか?を聞かれますが、今回は不要とします。「このプロジェクトで Google アナリティクスを有効にする」をオフにして、「プロジェクトを作成」をクリックします。

f:id:kun432:20191015190050p:plain

プロジェクトが作成されます。完了するまで待ちます。

f:id:kun432:20191015190304p:plain

完了したら、「続行」をクリックします。

f:id:kun432:20191015190314p:plain

以下の画面が表示されればプロジェクトの作成は完了です!

f:id:kun432:20191015190430p:plain

プロジェクトの設定(ユーザの作成)

では、Voiceflowから使えるようにプロジェクトの設定していきます。

まず、ユーザを作成します。ここでいうユーザは、Alexaスキルのユーザではなく、Firebaseへのアクセスを行うための、いわば特権ユーザになりますので注意してください。左のメニューの"Authentication"をクリックします。

f:id:kun432:20191015190724p:plain

Authenticationのページが開いたら、「ログイン方法を設定」をクリックします。

f:id:kun432:20191015190859p:plain

ログイン プロバイダの設定で、「メール/パスワード」にカーソルを合わせると、右に鉛筆アイコンが表示されますので、クリックします。

f:id:kun432:20191015191211p:plain

メールアドレス・パスワードでのログインを「有効」にして「保存」をクリックします。

f:id:kun432:20191015191417p:plain

メールアドレス・パスワードでのログインを「有効」になったことを確認したら、「ユーザ」をクリックします。

f:id:kun432:20191015191707p:plain

ユーザ画面が表示されたら、「ユーザを追加」をクリックします。

f:id:kun432:20191015191845p:plain

メールアドレスとパスワードを入力する画面が表示されます。なお、ここで入力するメールアドレスとパスワードは、あくまでもFirebaseへのアクセスの認証に使用するものなので、実際に存在するメールアドレスでなくてもかまいません。あとでVoiceflowからの接続時に必要になりますのでどこかに控えておいてください。入力したら「ユーザを追加」をクリックします。

f:id:kun432:20191015192013p:plain

メールアドレスが登録されたことを確認できればOKです。このとき、ユーザUUIDというランダムな文字列が発行されます。

f:id:kun432:20191015192828p:plain

UUIDにマウスカーソルを合わせると、コピーするためのアイコンが出てきます。これをクリックするとクリップボードにコピーされますので、これもどこかに控えておいてください。

f:id:kun432:20191015193011p:plain

これでユーザの作成は完了です。

プロジェクトの設定(Realtime Databaseの設定)

では、データベースを作成していきます。Firebaseでは2種類のデータベースが利用可能です。

  • Cloud Firestore
  • Realtime Database

Cloud Firestoreのほうが新しいのですが、今回はRealtime Databaseを使用します。

左のメニューから、"Database"をクリックします。

f:id:kun432:20191015193534p:plain

データベースの設定画面が開きます。すぐに「データベースの作成」が表示されていますが、これはCloud Firestoreの方なので注意してください。Realtime Databaseの方は、下にスクロールしたところにあります。

f:id:kun432:20191015193748p:plain

ここですね、こちらの方の「データベースを作成」をクリックします。

f:id:kun432:20191015194005p:plain

データベースのセキュリティルールの設定画面が出てきます。このルールは後で設定しますので、「ロックモードで開始」が選択されていることを確認の上、「有効にする」をクリックします。

f:id:kun432:20191015194151p:plain

データベースが作成され、データベースの設定画面が表示されます。ここでルールの設定を行います。上のメニューから「ルール」をクリックします。

f:id:kun432:20191015194550p:plain

ルール画面が開いたら、以下のように表示されているかと思います。ここに先程のUUIDを設定します。

f:id:kun432:20191016175913p:plain

以下のように入力してください。ユーザUUIDの部分は先ほど作成したUUIDで置き換えてください。ダブルクォートとシングルクォートが見にくいので気をつけてください。

{
  "rules": {
    ".write": "'ユーザUUID' === auth.uid",
    ".read": "'ユーザUUID' === auth.uid"
  }
}

入力したら「公開」をクリックします。

f:id:kun432:20191016180743p:plain

「ルールを公開しました」と表示されればOKです。

f:id:kun432:20191016180908p:plain

最後に、データベースにアクセスするためのAPIエンドポイントURLとAPIキーを取得します。まず、APIエンドポイントURLは上のメニューから「データ」をクリックします。

f:id:kun432:20191016181918p:plain

表示されているURLがAPIエンドポイントURLになりますので控えておいてください(ダブルクリックすると選択できます)。

f:id:kun432:20191016182721p:plain

次にWEB APIキーです。左上の歯車アイコンをクリックして、「プロジェクトの設定」をクリックします。

f:id:kun432:20191016182813p:plain

全般タブに「WEB APIキー」が表示されるので、これも控えておいてください。

f:id:kun432:20191016183245p:plain

はい、これでFirebase側の準備は完了です。

Voiceflowでスキル作成

いよいよVoiceflow側でスキルを作成していきます。あくまでもサンプルなので、単にスキルを起動・終了したら各種情報を保存するだけになっています。まず全体図を見てください。(ちなみに、VoiceflowのCanvasが新しくなりました。見た目が少し落ち着いた感じになり、高速かつスムーズな操作が可能になってます。一部UIも変わっていますので、それにあわせて説明しています。)

まず、メインフローです。一番最後にFlow Blockをおいて、Firebaseへのアクセスはその中でやっています。

f:id:kun432:20191022155438p:plain

Firebaseへのアクセスを行うFlow Blockの中身はこんな感じです。

f:id:kun432:20191022155450p:plain

ではそれぞれのフローを見ていきましょう。

メインフロー

f:id:kun432:20191022155935p:plain

まず、以下のプロジェクト変数を作ります。

f:id:kun432:20191022160116p:plain

それぞれの変数の意味は以下です。Voiceflow TIPS #1 Alexaの所在地情報を使う - kun432's blog で、Echoデバイスの所在地情報を取得した際とやってることはほぼ一緒です。今回は、所在地情報ではなく、Echoデバイスタイムゾーンを取得して日本における日時等を取得するために「Alexa設定API」を使います。

変数名 説明
deviceId バイスID。Alexa設定APIへアクセスするために必要。デバイスからのリクエストに含まれている。
apiAccessToken APIアクセストークン。Alexa設定APIへアクセスするために必要。デバイスからのリクエストに含まれている。
apiEndpoint APIエンドポイント。AAlexa設定APIへアクセスするために必要。デバイスからのリクエストに含まれている。
hasDisplay ディスプレイ対応有無
startTime スキル起動時の日時

今回これらをプロジェクト変数にしたのはFlow Blockから参照するためです。Voiceflowにおける変数のスコープについては、以前の記事を参考にしてください。

では各ブロックを見ていきます。

タイムゾーン情報取得に必要なデバイス情報や、スキル実行開始日時をCode Blockで取得

Code Blockで、デバイスタイムゾーンを取得するのに必要なデバイス情報や、スキル実行開始時間を取得しておきます。

f:id:kun432:20191022160254p:plain

コードはこんな感じです。

// デバイス情報取得
deviceId = _system.device.deviceId;
apiAccessToken = _system.apiAccessToken;
apiEndpoint = _system.apiEndpoint;
hasDisplay = voiceflow.capabilities && ('Alexa.Presentation.APL' in voiceflow.capabilities);

// 時刻情報取得
startTime = new Date();

このあたりは以前にご紹介した以下の記事でも紹介していますので、よろしければ参考にしてください。

② スキル起動時のメッセージを発話するSpeak Block

スキル起動時のメッセージを発話させます。最後の「続けますか?」というのを受けて、次のInteraction でユーザの発話を取ります。

f:id:kun432:20191022160459p:plain

③スキルをすすめるかどうかを確認するInteraction Block

前のSpeak Blockを受けて、「はい」と発話されたら次に進みます。それ以外の発話の場合は⑤でもう一度Interactionに戻して「はい」が発話させるまでループさせます。

f:id:kun432:20191022160618p:plain

⑤「はい」の発話を促すSpeak Block

順番前後しますが、③のInteractionで「はい」以外が発話された場合に、「はい」を言うように促します。ここで「はい」以外を繰り返すことで、スキル実行時間が変化することを確認するためです。

f:id:kun432:20191022160714p:plain

④ Firebaseアクセス処理を行うフローへのFlow Block

今回のテーマのメインであるFirebaseへのアクセスは、Flow Blockから"firebase flow"というフローを作ってそちらで処理を行うようにしています。"Enter Flow"で中身を見てましょう。

f:id:kun432:20191022160814p:plain

Firebaseにアクセスするfirebase flow

ここが今日のポイントですね。

f:id:kun432:20191022013110p:plain

まず最初にフロー変数を作ります。これらの変数はフローの中でしか使わないし、永続化する必要もないので、フロー変数にしています。

f:id:kun432:20191022161019p:plain

それぞれの変数の意味は以下です。

変数名 説明
firebase_url Firebase Realtime DatabaseのAPIエンドポイントURL(データベース作成時に取得したもの)
firebase_user Firebase Realtime Databaseへアクセスするためのメールアドレス(データベース作成時に取得したもの)
firebase_passwd Firebase Realtime Databaseへアクセスするためのパスワード(データベース作成時に取得したもの)
firebase_key Firebase Realtime DatabaseへアクセスするためのWEB APIキー(データベース作成時に取得したもの)
user_amazonId バイスから送られてきたユーザID
user_timezone Alexa設定APIから取得したユーザのタイムゾーン
user_sessionTime スキルの実行時間
user_startDate スキルの実行開始日
user_startTime スキルの実行開始時刻
user_endDate スキルの実行終了日
user_endTime スキルの実行終了時刻
firebase_idToken Firebaseへの認証後に返されるアクセストークン(これを使ってデータベースへの読み書きを行う)

では順にフローを見ていきます。

① Firebaseへのアクセス情報を設定しておくSet Block

Firebaseへのアクセスに必要な情報を変数にセットするためにSet Blockを使います。ここを間違えるとデータベースにアクセスができないので注意してください。

f:id:kun432:20191022161339p:plain

変数名 設定内容
firebase_url Firebase Realtime DatabaseのAPIエンドポイントURLを指定
firebase_user Firebaseで作成したユーザのパスワードを指定
firebase_passwd Firebaseで作成したユーザのパスワードを指定
firebase_key WEB APIキーを指定

②Alexa設定APIへのアクセスを行うIntegration Block & エラー時のメッセージを行うSpeak Block

まず、最初にAlexa設定APIへアクセスして、デバイスタイムゾーンを取得します。このあと出てくるCode Blockの中でJavaScriptを使って日時を取得するのですが、何も考えずに時刻を取得しようとするとJavaScriptが動いている実行環境、つまり、Voiceflow側のタイムゾーンアメリカになってしまいます。そこでデバイスが動作しているタイムゾーンをAlexa設定APIを使って取得した上で、それを踏まえてJavaScriptで日時を取得すれば、ユーザが利用している日時になるというわけです。

ということで、Alexa設定APIにアクセスするために、Integration BlockでCustom APIを使います。Custom APIタイプの設定をExpandしたメニューで説明しますね(Integration Blockの設定メニューの右上の歯車アイコンをクリックして、Expandを選択してください)。

f:id:kun432:20191022170728p:plain

それぞれの設定はこんな感じです。

f:id:kun432:20191022170908p:plain

  • リクエストURLは、種類は”GET”で、URLに ”{apiEndpoint}/v2/devices/{deviceId}/settings/System.timeZone” を指定します。
  • リクエストパラメータは、"Headers"タブに以下を設定します。
    • "Enter HTTP Headers" に "Authorization" を入力
    • "Value" に "Bearer {apiAccessToken}" を入力
  • 結果を変数にマッピングします。"Transform into Variables"に以下を設定します。
    • "Enter object path" に "response"を入力
    • "APPLY TO" で 変数"user_timeZone" を選択

で、Test Integrationからテストしたいところですが、残念ながらこのAPIはEchoデバイス経由でないと動作しないので、ここではテストを割愛します。

最後にfailからSpeak Blockをつなげて、APIリクエストが失敗した場合のエラーメッセージを発話させるようにします。一旦はこのままにしておいて動作確認が取れたら最後にちょっとだけいじります。

f:id:kun432:20191022171525p:plain

タイムゾーンを使って日時取得を行うCode Block

②で取得したタイムゾーン情報を踏まえて、JavaScriptで日時情報やスキルの実行時間を取得します。

f:id:kun432:20191022171639p:plain

コードはこんな感じです。

// AmazonユーザID
user_amazonId = user_id.split(".");
user_amazonId = user_amazonId[3];

// 開始・現在・経過をDateオブジェクトで取得
var now      = new Date();
var start    = new Date(startTime);
var duration = Math.abs(now.getTime() - start.getTime())/1000;

// 経過時間取得
var hours   = Math.floor(duration / 3600) % 24;
var minutes = Math.floor(duration / 60) % 60;
var seconds = duration % 60;
user_sessionTime = hours+":"+minutes+":"+Math.round(seconds);

// 日時・時刻取得
user_startTime = new Date(startTime).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', second: '2-digit', timeZone: user_timeZone });
user_startDate = new Date(startTime).toLocaleDateString(locale, { year: 'numeric', month: '2-digit', day: '2-digit', timeZone: user_timeZone });
user_endTime   = new Date().toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', second: '2-digit', timeZone: user_timeZone });
user_endDate   = new Date().toLocaleDateString(locale, { year: 'numeric', month: '2-digit', day: '2-digit', timeZone: user_timeZone });

少しだけ解説しておきます。

  • 1〜3行目でEchoデバイスから取得したAlexaのユーザIDを少しだけいじります。ユーザIDはamzn1.ask.account.XXXXXXX・・・となっており、amzn1.ask.accountは共通となるため、取り除いている感じです。
  • 5〜14行目で、メインフローの最初で取得したスキルの実行開始日時と現在の日時を比較して、スキルの実行時間を計算してます。
  • 16行目以降で、スキル実行開始日時と終了日時をそれぞれ文字列として使えるように整形しています。

エラー時の処理はintegration Blockと同じような感じです。

f:id:kun432:20191022171830p:plain

④ Firebaseへアクセスするための認証を行うIntegration Block

いよいよFirebaseにアクセスしていきます・・・といいたいところですが、Firebaseへのアクセスは、最初に認証を行い、認証成功後に受け取るトークンを使ってデータベースへの読み書きを行う形となります。まずはFirebaseへの認証処理をIntegration Blockで設定していきます。

f:id:kun432:20191022171952p:plain

順に設定をExpandメニューで見ていきます。

f:id:kun432:20191022172604p:plainf:id:kun432:20191022172614p:plain

{
    email: "{firebase_user}",
    password: "{firebase_passwd}",
    returnSecureToken: true
}
  • 結果を変数にマッピングします。"Transform into Variables"に以下を設定します。
    • "Enter object path" に "response.idToken"を入力
    • "APPLY TO" で 変数"firebase_idToken" を選択

BodyタイプでRawを指定した場合、Bodyに変数が含まれていても、Test Integration では指定できないので、ここもテストはできません。割愛します。

エラー時の処理はこれまでと同じような感じです。

f:id:kun432:20191022172902p:plain

⑤ Firebaseへ書き込みを行うためにIntegration Block

④で取得したトークンを使ってデータベースに書き込みを行います。Integration BlockのCustom APIを使います。

f:id:kun432:20191022174506p:plain

設定はこんな感じです。

f:id:kun432:20191022174720p:plainf:id:kun432:20191022174735p:plain

  • リクエストURLは、種類は”PATCH”で、URLに ”{firebase_url}/users/{user_amazonId}.json” を指定します。
  • リクエストパラメータは、"Headers"タブに以下を設定します。
    • "Enter HTTP Headers" に "Content-Type" を入力
    • "Value" に "application/json" を入力
  • "Body"タブをクリックして、以下を設定します。
    • タイプは"Raw"を設定して、以下を入力します。
{
    "userID": "{user_id}",
    "timestamp": {timestamp},
    "datetime_start": "{user_startDate} {user_startTime}",
    "datetime_end": "{user_endDate} {user_endTime}",
    "session_time": "{user_sessionTime}",
    "hasDisplay": {hasDisplay},
    "sessions": {sessions},
    "userTimezone":"{user_timeZone}"
}

結果を変数で取得する必要はありません。ここもテストはできませんので割愛します。エラー時の処理も同じような感じで設定してください。

f:id:kun432:20191022175158p:plain

⑥ スキル終了の最後のメッセージを発話するSpeak Block

エラーが発生しなければ、ここにきますので、スキルの終了を発話させます。

f:id:kun432:20191022175506p:plain

はい、これで終了です

テスト

では開発者コンソールから早速テストしてみましょう。

f:id:kun432:20191022175739p:plain

サンプルなのでこれだけです。「登録しました」と出ているので、一通りの処理は成功しているようですね。Firebaseの方にも登録されているか見てみましょう。

f:id:kun432:20191022180157p:plain

ユーザ情報が登録されました。初めてのセッション、起動時間が6秒間、ディスプレイなし、タイムゾーンが日本、になっていますね。

もう一度テストしてから見てみるとこうなります。

f:id:kun432:20191022180553p:plain

セッションが2回めになっていて、今回は起動時間が変わっているのがわかりますでしょうか。ちなみに、hasDisplayはDisplay Blockを置かないと値が取れません。今回のサンプルではDisplay Blockを置いてないのでこの値は変わりませんので、実際にディスプレイ対応スキルを作る際の参考だと思ってください。

おまけ

Firebase Flowの中で、各処理ごとにSpeak Blockでエラー内容を発話させていましたが、動作が確認できればこの処理は消しちゃっても良いと思います。今回の例では、あくまでもユーザ情報を取得するのが目的なので、エラーをいちいちユーザに伝える必要はないということで。取れない場合はしょうがないという割り切りですね。もちろん使い方によっては、きちんとエラー処理を行わないといけない場合もあると思いますので、ケースバイケースで判断してください。

今回はこんな感じでまるっと終了させてみました。

f:id:kun432:20191022184027p:plain

まとめ

今回始めてFirebase使ってみたのですが、認証やAPIが予め用意されていてさらっと外部のデータベースを使いたい、という場合にとても良いですね。Voiceflowのプロジェクト変数やGoogleスプレッドシートを使っての管理もお手軽でいいのですが、こちらのほうがより高速で安定していて管理もしやすそうなので、いろいろ活用の幅が広がりそうです。ぜひトライしてみてください。

最後に、今回の参考にさせていただいた素晴らしいチュートリアルの作者のNicolas Arcay Bermejo(@ArcayBermejo)さんに感謝! Nicolasさんはこれ以外にも多数の素晴らしいチュートリアル記事を書かれていたり、Community Marketplaceにも多数デモスキルを提供されていますので、ぜひぜひ見てみてください!