LINE Clova + Firebase + Vue.js

この記事はLINE Advent Calendar 2018の19日目の記事です。

こんにちは、LINE Ads Platformの開発チームに所属している新卒1年目の佐藤邦彦です。

本記事では、FirebaseとVue.jsを利用してLINE ClovaとWebアプリケーションを連動させる方法を紹介します。

LINE ClovaとWebアプリケーションを連動させる

今年の7月からLINE Clovaのスキル開発環境がオープン化され、だれでもClovaのスキルを開発できるようになりました。

スキル開発のオープン化によってClovaにさまざまな機能を追加できます。また、開発したスキルとLINE botを連携することも可能です。LINE botと連携することでユーザーに対してより多くの機能を提供できるようになります。一方で、LINE botとの連携でできることは、LINE botの機能やUIに限定されてしまいます。

そこで本記事ではClovaスキルと任意のWebアプリケーションを連携させる方法を紹介します。ClovaスキルをWebアプリケーションと連携することで、LINE botとは異なった機能やUIを提供できるようになります。

今回の記事では、簡単なシングルページアプリケーションとClovaスキルの連携を紹介します。今回実装するシステムは、Clovaに話しかけた言葉を文字に書き起こしWebアプリケーション上に表示します。例えば、以下の図のように、Clovaに「おはよう」と話しかけるとWebアプリケーション側に”おはよう”と文字が表示されます。

本記事では、Webアプリケーションのユーザー認証とデータの保存をFirebaseを用いて行います。Firebaseを用いることでユーザー認証やデータベース用のサーバーを自前で用意する必要がないため、サービスの立ち上げを迅速に行えます。

また、WebアプリケーションのフレームワークとしてVue.jsを使用します。

デモ

今回開発したシステムの実際のデモ映像です。今回開発したClovaスキル名を「書き起こし」としました。

「書き起こし」スキルを起動したあと、Clovaに向かって「おはよう」と言うと”おはよう”という文字がWeb画面上にリアルタイムで書き起こされます。

実装したコードはGitHub上で公開しています。この後の実装の説明と合わせながら適宜参照してくださいませ。

Webアプリケーションの実装

Clovaに入力された言葉を表示するシングルページアプリケーションを実装していきます。フレームワークにはVue.jsを使用します。Vue.jsは近年もっとも人気のあるJavaScriptフレームワークのひとつで、私も業務でVue.jsを使っています。

Vue.jsのインストールとFirebaseを利用したユーザー認証の実装

まずはVue.jsのインストールとFirebaseを利用したユーザー認証の実装を行います。

Vue.jsのインストールとユーザー認証の実装は以下のブログを参考に行いました(以下のブログは英語で書かれていますが、わかりやすい英文で書かれているため英語が苦手でも読んでみることをおすすめします)。

Basic Single Page application using Vue.js and Firebase — Part 1

上記のブログはpart2まであります。part2までの内容に沿って実装を進めるとユーザー認証の実装が完了します。

補足として、上記ブログではメールアドレスを用いた認証方式を採用しているため、以下の画像のようにFirebaseのコンソール上でメールでのログインを有効にする必要があります。

また、上記のブログは2017年に公開された内容であるため、Firebaseのバージョンが古いです。そのため、現在のバージョンであるFirebase JavaScript SDK 5系を利用する場合は/src/main.jsに以下の追記が必要です。

Vue.use(Vuetify)
firebase.initializeApp({
  apiKey: 'YOUR_API_KEY',
  authDomain: 'YOUR_AUTH_DOMAIN',
  databaseURL: 'YOUR_DATABASE_URL',
  projectId: 'YOUR_PROJECT_ID'
})
/* 以下の2行を追記 */
const settings = {timestampsInSnapshots: true}
firebase.firestore().settings(settings)

Vue.config.productionTip = false

また、/src/main.js上にFirebaseのAPIキーなどの情報をハードコーディングすることは、セキュリティやコード管理の観点からよいことではありません。そこでFirebaseのinitializeApp()に必要な情報を/config/dev.env.jsに書きます。

'use strict'
const merge = require('webpack-merge')
const prodEnv = require('./prod.env')

module.exports = merge(prodEnv, {
  NODE_ENV: '"development"',
  /* 以下の行にFirebaseの情報を追加。実装の際はValueに実際の値を入れてください。*/
  API_KEY: 'YOUR_API_KEY',
  AUTH_DOMAIN: 'YOUR_AUTH_DOMAIN',
  DATABASE_URL: 'YOUR_DATABASE_URL',
  PROJECT_ID: 'YOUR_PROJECT_ID'
})

/config/dev.env.jsはGitHub等で公開しないように.gitignoreに追加しておきます。 加えて、/src/main.jsfirebase.initializeApp()を以下のように変更します。

...

/* Write your firebase setting */
firebase.initializeApp({
  apiKey: process.env.API_KEY,
  authDomain: process.env.AUTH_DOMAIN,
  databaseURL: process.env.DATABASE_URL,
  projectId: process.env.PROJECT_ID
})

...

なお、Vue CLIの3系が正式にリリースされたことに伴い、環境設定の書き方が少し変わったそうです(https://cli.vuejs.org/guide/mode-and-env.html#modes)。今回のシステムはVue CLI 3のbetaを使用しているため、正式リリースされた3系と異なることがあります。

コマンドラインでnpm run dev(パッケージマネージャーにYarnを使った場合はyarn dev)を実行し、アプリケーションを立ち上げます。デフォルトではlocalhost:8080でアプリケーションが立ち上がります。ユーザー認証が正しく動作すると以下のGIFのようになります。Sign inをすると特に何も記述されていないページ(Home.vue)が表示されます。

Firebaseを利用したデータベースの設定

今回開発するシステムでは、Clovaに入力した言葉をFirebase上のデータベースに保存し、その保存した内容をWebアプリケーション側にリアルタイムで反映させます。

Firebaseでは2種類のデータベース機能を提供していますが、今回はCloud Firestoreを利用します。

Firebaseのコンソール上でCloud Firestoreの設定をします。まず、データベースを追加します。データベース作成時にCloud Firestoreのセキュリティルールとしてロックモードかテストモードのどちらかを選択する必要があります。ひとまずテストモードを選択します。

データベース作成後、「コレクションを追加」を選択します。今回の開発では、コレクションIDを「items」にしました。

「次へ」を押したあと、ドキュメントの作成を行います。以下の画像のように、「message」フィールドと「timestamp」フィールドを設定しました。「message」はClovaに入力された言葉を保存するためのフィールドです。「timestamp」はドキュメントがデータベースに保存された時刻を記録するためのフィールドです。

保存ボタンを押してCloud Firestoreの設定は完了です。

データベースの値の取得

Cloud Firestoreに保存した値をWebアプリケーション側で取得する方法を説明します。

ユーザー認証を実装済みの場合、Webアプリケーションのソースコードのフォルダ構造は以下のようになっていると思います。

今回のシステムではHome.vue上でデータベースの取得と表示を行います。Home.vueを編集し、データの表示のコードを以下のように実装します。

<template>

  <v-data-table
    :headers="headers"
    :items="displayItems"
    class="elevation-1"
  >

    <template slot="headers" slot-scope="props">
      <th class="text-md-center">{{ props.headers[0].value }}</th>
    </template>

    <template slot="items" slot-scope="props">
      <td class="text-xs-center">{{ props.item.message }}</td>
    </template>
      
    <template slot="no-data">
      <v-btn color="primary" @click="loadData">Reload</v-btn>
    </template>

  </v-data-table>
    
</template>
	
<script>
import firebase from 'firebase'
import 'firebase/firestore'

export default {
  data () {
    return {
      data: [],
      headers: [
        { text: 'message', value: 'message', sortable: false }
      ]
    }
  },
  computed: {
    displayItems () {
      return this.data
    }
  },
  //以下の行に後からコードを追加します。
}
</script>

UIデザイン用のフレームワークVuetifyのコンポーネントであるv-data-tableにデータベースの値を入れて表示します。v-data-tableタグ内の:itemsがテーブルの行ごとのコンテンツを表します。上記コードでは:itemscomputed内のdisplayItems()がバインディングされています。displayItems()data()内のdata:[]を返します。data:[]には、後述の実装によってデータベースの値が入ります。

次にCloud Firestoreから値(コレクションとドキュメント)を取得するコードを実装します。上記のコードの下に以下のコードを追記します。

...
<script>
import firebase from 'firebase'
import 'firebase/firestore'

export default {
  data () {
    return {
      data: [],
      headers: [
        { text: 'message', value: 'message', sortable: false }
      ]
    }
  },
  computed: {
    displayItems () {
      return this.data
    }
  },
  //以下の行を追加
  created () {
    this.loadData()
  },
  methods: {
    /* Sync database */
    loadData () {
      const ref = firebase.firestore().collection('items')
        .orderBy('timestamp')
      ref.onSnapshot(querySnapshot => {
        this.data = []
        querySnapshot.forEach(doc => {
          const message = {
            id: doc.id,
            message: doc.data().message
          }
          this.data.push(message)
        })
      })
    }
  }
}
</script>

firebase.firestore().collection()で任意のコレクションIDのドキュメントを取得できます。そのあとの.orderBy('timestamp')でドキュメントをタイムスタンプ順にソートしています。

ref.onSnapshot()でドキュメントをリッスンしています。つまり、ドキュメントに変更が発生すると更新されたドキュメントを自動で取得します。取得した値はdata:[]に格納されます。data:[]の値が変更されるとdisplayItems()が更新され、displayItems()にバイディングされた:itemsが自動で更新されます。結果として、Webブラウザ上の表示が更新されます。

Cloud Firestoreの値の取得には、get()メソッドもありますが、こちらはドキュメントの変更に対してコールバックされないので注意してください。onSnapshot()メソッドなどに関しては公式ドキュメントにも詳しく書かれています(https://firebase.google.com/docs/firestore/query-data/listen)。

以上でCloud FirestoreとWebアプリケーションの連携ができました。

Firebaseのコンソール上でCloud Firestoreの中身を編集し、Webアプリケーション側に編集結果が反映されるか確認してみます。以下のGIFは実際の動作確認の様子です。GIFの左側がFirebaseコンソールで右側がWebアプリケーションです。FirebaseコンソールのDatebase項目で「ドキュメントを追加」を選択します。messageフィールドの値に「こんにちは」と入力し、timestampフィールドに適当な値を入れます。「保存」ボタンを押したと同時に、画面右のWebアプリケーション側の表示内容が自動で更新され「こんにちは」という文字が新しく追加されれば正しく動作しています。

セキュリティルールの設定

ここまでの実装ではCloud Firestoreはテストモードで実行されています。テストモードではすべてのドキュメント(Cloud Firestoreにおけるデータのこと)へのあらゆる操作を許可しているため、セキュリティインシデントが発生する可能性が高いです。データのセキュリティを担保するためにはクライアント側(Webアプリケーション側)からのアクセスとデータ操作を制限する必要があります。それぞれの制限のために、Cloud Firestoreのセキュリティルールを設定します。

FirestoreのセキュリティルールはFirebaseコンソールから確認できます。現在のセキュリティルールは以下の画像のようになっていると思います。

このセキュリティルールを以下のように変更します。

上記のルールで設けている条件は以下の通りです。

  • Cloud Firestore上のデータベースににアクセスできるのは認証ユーザーのみ
  • 認証ユーザーがアクセスできるデータは「items」コレクションと「items」が持つドキュメントのみ
  • 認証ユーザーは「items」コレクションとそのドキュメントに対して読み取りのみ可能

今回のシステムではWebアプリケーション側からデータの書き換えや削除は行わないので読み取りのみ可能にしています。

Cloud Firestoreのセキュリティルールの詳細については公式ドキュメントを参照ください(https://firebase.google.com/docs/firestore/security/get-started?hl=ja)。

Clovaスキルの実装

Clovaスキルを開発するためのくわしい手順については公式ドキュメント等を参照ください。ここでは今回実装するシステムで必要な設定を説明します。なお、今回実装したClovaスキルはサンプル用のため、Clovaスキルストアで公開はしていません。

Clovaスキルの基本情報の設定

開発するスキルの基本情報をClova Developer Centerで設定します。Extension IDやスキル名等は以下のように登録しました。

サーバー設定の「ExtensionサーバーのURL」についてはあとで設定するので、とりあえずhttps://hogeのように適当に埋めておいてください。

対話モデルの作成

Clovaスキルの実装でひとつ注意すべきことは、Clovaによって音声認識された文字列すべてをClovaスキルによって取得できるわけではないということです。Clovaスキルによって取得できる文字列は対話モデルに設定されたスロットのみです。

今回の実装はサンプルなので、とりあえず「おはよう」「こんにちは」「おやすみ」の三語をスロットに登録し、これらの言葉のみを書き起こしできるようにします。

カスタムスロットの作成は以下の画像のようになります。greetingスロットを作成し、上記の三語を代表語として登録します。

インテントの作成は以下のようになります。

スロットとインテントを作成したらビルドします。

Extensionサーバーの実装

ExtensionサーバーをClova上で動かす

Node.jsを用いてClovaスキルのExtensionサーバーを実装します。

まず、Extensionサーバー用のフォルダを作ります。今回のシステムではclova-extension-serverフォルダを作りました。

Node.js版のClova SDKが公開されていますのでこれを利用します(https://github.com/line/clova-cek-sdk-nodejs)。clova-extension-serverフォルダ内で以下のコマンドで実行しClova SDKをインストールします。

npm init
npm install --save @line/clova-cek-sdk-nodejs

その他、必要になるSDK等を以下のコマンドでまとめてインストールします。

npm install --save body-parser express firebase firebase-admin

Extensionサーバー用のコードを記述します。/clova-extension-serverの内にindex.jsを作成します。index.jsには上記のClova SDKのREADMEに書いてあるExampleをそのままコピペします。さらに、コピペした後に 以下のコードを最後の行に追加します。

...

//以下のコードを追加
if(!isNaN(process.env.PORT_APP)) {
    port = process.env.PORT_APP;
} else {
    port =3000;
}
console.log(port)
app.listen(port)

これでindex.jsの実行が可能になりました。以下のコマンドで実行をします。

node index.js

おそらくポート番号3000で実行されると思います。

このコードを実際にClova上で動作するように設定します。今回はとりあえずテスト的に実行するためクラウド環境等を使わずにローカルで実行します。Clova ExtensionサーバーではHTTPSに対応したURLがに必要になります。ローカルサーバーに対して一時的にURLを割り当てるためにngrokを利用します。ngrokをインストールし、以下のコマンドを実行します。

ngrok http 3000

ローカルホストにURLが割り当てられます。上記のコマンドを実行後にターミナル上に表示されるHTTPSのほうのURLをコピーし、Clova Developer Centerのスキル基本情報の「ExtensionサーバーのURL」欄にペーストします。URLの最後に/clovaをつけるのを忘れないでください。

ExtensionサーバーからFirebaseのデータベースに書き込む

Extensionサーバーで取得したスロットの値をFirebaseのデータベース(Cloud Firestore)に書き込むための実装を行います。

Firebase Admin SDKを利用できるように、index.jsに認証のためのコードを追加します。

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

const serviceAccount = require('./key/serviceAccountKey.json');

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount)
});

上記コードで読み込んでいる'./key/serviceAccountKey.json'はFirebase Admin SDKを利用するための秘密鍵が書かれたJSONファイルです。このJSONファイルはFirebaseコンソールからダウンロードできます。Project Overviewの横にあるアイコンをクリックし「プロジェクトの設定」を選択します。その後、上のタブにある「サービスアカウント」を選択すると秘密鍵を新しく生成するための画面が表示されます。

「新しい秘密鍵の生成」というボタンを押すと上記のJSONファイルがダウンロードできます。ダウンロードしたファイルをindex.js以下の./keyフォルダ内にserviceAccountKey.jsonとして保存します。当ファイルには秘密鍵が書かれているため、間違ってGitHub等にpushしないように気をつけてください。

これでExtensionサーバーでFirebase Admin SDKの利用が可能になりました。なお、Extensionサーバーをローカルでしか動かす予定がない場合、あるいはGoogle Cloud Platform上で動かす場合は上記のファイルを利用せずに、以下のコードでFirebase Admin SDKを認証することができます。

admin.initializeApp({
  credential: admin.credential.applicationDefault(),
  databaseURL: 'https://<DATABASE_NAME>.firebaseio.com'
});

詳しくはFirebaseの公式ドキュメントを参照ください(https://firebase.google.com/docs/admin/setup?hl=ja)。

次に、index.jsにデータベースへの書き込み処理を追加します。

function write_database(message) {

    const timestamp = admin.firestore.FieldValue.serverTimestamp();
    
    const db = admin.firestore();
    // Add a new item with a generated id.
    const addDoc = db.collection('items').add({
        message: message,
        timestamp: timestamp,
    }).then(ref => {
        console.log('Added item with ID: ', ref.id);
    });
}

admin.firestore.FieldValue.serverTimestamp()でデータベースへの書き込み時のタイムスタンプを取得しています。db.collection('items')で書き込む対象のコレクションIDを選択し、add()で新しいドキュメントを追加しています。

write_database(message)を任意のタイミングで呼び出すことでデータベースへの書き込みを行えます。index.jsの全コードは以下のようになります。

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

const serviceAccount = require('./key/serviceAccountKey.json');

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount)
});

const clova = require('@line/clova-cek-sdk-nodejs');
const express = require('express');
const bodyParser = require('body-parser');

const clovaSkillHandler = clova.Client
  .configureSkill()
  .onLaunchRequest(responseHelper => {
    responseHelper.setSimpleSpeech({
      lang: 'ja',
      type: 'PlainText',
      value: '書きおこしスキルを起動しました',
    });
  })
  .onIntentRequest(async responseHelper => {
    const intent = responseHelper.getIntentName();
    const sessionId = responseHelper.getSessionId();
    switch (intent) {
      case 'test':
        const slot = responseHelper.getSlots()["greeting"]
        responseHelper.setSimpleSpeech({
          lang: 'ja',
          type: 'PlainText',
          value: slot + 'と書き起こしました',
        });
        write_database(slot)
        break;
    }
  })
  .onSessionEndedRequest(responseHelper => {
    const sessionId = responseHelper.getSessionId();

    // Do something on session end
  })
  .handle();

const app = new express();
const clovaMiddleware = clova.Middleware({ applicationId: "com.clova.firebase.vue" });
// Use `clovaMiddleware` if you want to verify signature and applicationId.
// Please note `applicationId` is required when using this middleware.
app.post('/clova', clovaMiddleware, clovaSkillHandler);

// Or you can simply use `bodyParser.json()` to accept any request without verifying, e.g.,
app.post('/clova', bodyParser.json(), clovaSkillHandler);

if(!isNaN(process.env.PORT_APP)) {
    port = process.env.PORT_APP;
} else {
    port =3000;
}
console.log(port)
app.listen(port)

function write_database(message) {

    const timestamp = admin.firestore.FieldValue.serverTimestamp();
    
    const db = admin.firestore();
    // Add a new item with a generated id.
    const addDoc = db.collection('items').add({
        message: message,
        timestamp: timestamp,
    }).then(ref => {
        console.log('Added item with ID: ', ref.id);
    });
}

以上の実装で全て完了です。

まとめ

本記事では、Clovaに話しかけた言葉をWeb画面上に表示するシステムの実装方法を紹介しました。

今回作成したシステムは非常にシンプルなため、より複雑なアプリケーションに対応するためには実装すべき機能がまだまだあります。例えば、今回のシステムでは、「書き起こし」スキルに話しかけた全ての人の言葉が、ログイン済みのユーザー全てのWeb画面上に表示されてしまいます。もし、ClovaとWebアプリケーションのユーザーアカウントを1対1で結びつけたい場合は、LINEログインの実装を行う必要があります。今回の記事ではLINEログインの実装は紹介しませんでしたが、開発したいアプリケーションに合わせてぜひ実装してみてくださいませ。

記事の内容に何か間違い・コメントがありましたら、GitHub or Twitterまでお願いいたします。

明日の記事はKang Yuさんによる「Adventures of using Kafka Streams」です。お楽しみに!

Related Post