LoginSignup
32
16

More than 5 years have passed since last update.

Google HomeとSalesforceをAccount Linkingで連携した(動画あり)

Last updated at Posted at 2017-12-05

Quip Live Appsの何かを書くと予告していたのですが、すみません。予告詐欺です。

Google Homeから音声でSalesforceを操作するGoogle Homeアクションを作ってみることになりました。

今回作るアクションのシナリオ

  1. Salesforceのアカウントと紐付ける
  2. ユーザ「Salesforceアプリにつないで」
  3. ボット「こんにちは。Salesforceアプリです。」
  4. ユーザ「今日の予定は?」
  5. ボット「今日の予定はXXXです。」

みたいなのを作ろうと思います。

内容はとりあえず連携を試すためのものではあります。
しかし、実はGoogle Homeは個人用Gmailアカウントとリンクしており、Googleカレンダー連携機能が役に立たないため、Salesforceで予定が確認できると個人的に便利なのではないかと期待もしています。

動くものをすぐに見てみたい方は下の方に動画がありますので、そちらをご覧ください。

Account Linkingの設定

Salesforceと連携するにはどうしたってユーザ認証する必要があります。
Google HomeにはAccount Linkingsという仕組みがあるのでこれを利用します。

Salesforce側の設定

以下のような設定で接続アプリケーションを作成します。

image.png

  • コールバックURL: https://oauth-redirect.googleusercontent.com/r/<google developer project ID>
  • 選択したOAuth範囲: full(テストなので)、refresh_token

Actions Consoleの設定

Dialogflowでシミュレータが動くところまでやっておきます。

Overviewから(5)Account Linkingを選び[Add]をクリック

image.png

順番に入力していきます。

Dialogflowを開き、[Sign in required for welcome intent]にチェック

image.png

認証連携の確認

シミュレータで確認すると、「まだリンクされていません」と表示されます。
(この状態でGoogle Home実機に話しかけても同じことになります。)

[DEBUG]タブにURLが表示されるので、そのURLをブラウザで開きます。
(Google Homeアプリからもできるらしいのですが、見当たりませんでした。開発版ではできないのだろうか?)

image.png

Salesforceへのログイン画面が表示されるのでログイン。

シミュレータに戻って再度アプリを呼び出すと、今度はちゃんと動き、accessTokenが取れていることがわかります。
(この状態でGoogle Home実機に話しかけても同じことになります。)

image.png

アプリの実装

アカウント連携ができたので、アプリを実装していきます。
今回はDialogflowとCloud Functions for Firebaseを使っていきます。

Entities

エンティティ名 内容
end 終了コマンド
object Salesforceのオブジェクト名
what-to-know 「教えて」や「知りたい」という単語

end

image.png

object

image.png

what-to-know

image.png

正直、どんな粒度でどのように定義すればいいかよくわかっていません。

Intents

インテント名 内容
Default Welcome Intent アプリを起動したとき
Default Fallback Intent わからないとき
GetRecordsByDate メイン
日付とオブジェクト名でSalesforceのレコードを取得します。
End 終了するとき

Default Welcome Intent

Text Responseを変更しただけでそれ以外はデフォルトのままです。

image.png

Default Fallback Intent

こちらは後でFulfillmentから呼ぶためにEvent名をつけました。
あとはText ResponseをGoogle Homeらしく変更しただけです。

image.png

GetRecordsByDate

メインの機能です。
@sys.date@object@want-to-know」のように定義しています。
そうすると、あとでFulfillmentでパラメータとして受け取れます。

@sys.dateは定義済みの日付に対応するエンティティです。「今日」「4日前」「一週間後」「来週の月曜」なども解釈してくれるのでなかなか便利です。
定義済みのエンティティはSystem Entitiesと言って他にもいろいろあります。

最後に[Fulfillment]>[Use webhook]にチェックを入れます。これでこのintentのときはFulfillmentで処理をするようになります。

image.png
image.png

End

@endエンティティのときに呼ばれるようにします。
[Google Assistant]>[End conversation]にチェックを入れると、このインテントが処理されると会話を終了するようになります。

image.png
image.png

Fulfillment

楽なのでInline EditorでCloud Functions for Firebaseを使います。
今回はSalesforceのREST APIを呼びます。無料プランだとGoogle外のドメインにリクエストを投げられませんので、従量課金のBlazeプランに変更する必要があります(Firebaseの価格表)。

package.json

いつもお世話になっているJSforceを追加します。

    "jsforce": "^1.8.0"

image.png

index.js

以下のようにしました。
基本的にはInline Editorを有効にしたときに生成されるデフォルトコードをベースに修正しています。Dialogflow's V2 APIはまだactions-on-googleのnpmが対応していなそうだったので、v2のコードはごっそり削除しています。

index.js
'use strict';

const functions = require('firebase-functions'); // Cloud Functions for Firebase library
const DialogflowApp = require('actions-on-google').DialogflowApp; // Google Assistant helper library
const jsforce = require('jsforce');
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
  console.log('Dialogflow Request headers: ' + JSON.stringify(request.headers));
  console.log('Dialogflow Request body: ' + JSON.stringify(request.body));
  processV1Request(request, response);
});
/*
* Function to handle v1 webhook requests from Dialogflow
*/
function processV1Request (request, response) {
  let action = request.body.result.action; // https://dialogflow.com/docs/actions-and-parameters
  let parameters = request.body.result.parameters; // https://dialogflow.com/docs/actions-and-parameters
  let inputContexts = request.body.result.contexts; // https://dialogflow.com/docs/contexts
  let requestSource = (request.body.originalRequest) ? request.body.originalRequest.source : undefined;
  const app = new DialogflowApp({request: request, response: response});
  // Create handlers for Dialogflow actions as well as a 'default' handler
  const actionHandlers = {
    'input.get_records_by_date': () => {
      const conn = new jsforce.Connection({
        accessToken: app.getUser().accessToken,
        instanceUrl:'https://ap3.salesforce.com/'
      });
      const date = parameters.date;
      const object = parameters.object;
      const objectDef = {
        event: {
          label: '予定',
          query: `SELECT Subject, FORMAT(StartDateTime) 
          FROM Event USING SCOPE Mine 
          WHERE ActivityDate = ${date}
          ORDER BY StartDateTime ASC LIMIT 10`,
        },
        task: {
          label: 'タスク',
          query: `SELECT Subject FROM Task USING SCOPE Mine 
          WHERE (ActivityDate < ${date} OR ActivityDate = null)
          AND IsClosed = false
          LIMIT 10`,
        }
      };
      const def = objectDef[object];
      conn.query(def.query)
      .then((result) => {
        console.log(result);
        let res = '<speak>';
        if (result.records.length === 0) {
          res += `<p>${def.label}はありません</p>
          <break time="0.5s" />
          <p>今日もがんばりましょう!</p>
          </speak>`;
        } else {
          result.records.forEach((record) => {
            res += '<p>';
            if (record.StartDateTime)
              res += `<s>${record.StartDateTime.substr(11, 5)}</s>`;
            res += `<s>${record.Subject}</s></p>`;
          });
          res += `<break time="0.5s" />
          <p>以上です。</p>
          <p>今日もがんばりましょう!</p>
          </speak>`;
        }
        console.log(res);
        // Use the Actions on Google lib to respond to Google requests; for other requests use JSON
        app.ask(res);
      })
      .catch((e) => {
        console.error(e);
        app.ask('すみません。失敗してしまいました。');
      });
    },
    // Default handler for unknown or undefined actions
    'default': () => {
      response.send(
        JSON.stringify({
          followupEvent: {
            name: "UNKNOWN"
          }
        })
      );
    }
  };
  // If undefined or unknown action use the default handler
  if (!actionHandlers[action]) {
    action = 'default';
  }
  // Run the proper handler function to handle the request from Dialogflow
  actionHandlers[action]();
}

動かしてみよう

シミュレータで動かす

このように動きます。
なお、いつの間にかアプリ名を「Salesforceアプリ」に変更しています。

image.png

Google Homeで動かす

実機だとこんな風に動きます。動画です。画像をクリックするとYouTubeに飛びます。

image.png
http://www.youtube.com/watch?v=9JGGJpPEsD4

インコは黙ってもらうことができませんでした。

なお、Google Homeは6人まで声を識別するので、それぞれ別のSalesforceアカウントに紐付けることができるはずです(試してないけど)。

問題点

SalesforceのインスタンスURLをハードコードしてしまっている

index.jsに一箇所SalesforceのインスタンスURLだけハードコードしてしまっています。
普通のサービスだとAPIエンドポイントが一つなのですが、Salesforceは組織によってAPIエンドポイントが変わります。
本来はアクセストークン取得リクエストのレスポンスで取得できるのですが、Account Linkingの中で自動的に処理してくれてしまっていて、それを取り出す方法が見つかりませんでした。

      const conn = new jsforce.Connection({
        accessToken: app.getUser().accessToken,
        instanceUrl:'https://ap3.salesforce.com/'
      });

アクセストークンの更新が動かない

実用には致命的なのですが、アクセストークンの有効期限(Salesforceの組織で設定)を過ぎると動かなくなってしまいます。Cloud FunctionsのログにはINVALID_SESSION_ID: Session expired or invalidとエラーが出力されています。

認証周りのエラーはどこにもログが出てないようなので何が悪いのかわからないのですが、きっとトークンの更新が動いてないのでしょう。
Account Linkingの要件SalesforceのOAuthの仕様を見比べるとSalesforceはexpires_inを返していないので、いつアクセストークンの期限が切れるかわからなくてトークンの更新処理をしないのかなあと予想しています。

おわりに

とりあえずGoogle Homeから音声でSalesforceを扱うことができました。
ただし、アカウント連携はAccount Linkingをそのまま使うのは難しそうなので、ちょっと考える必要がありそうです。誰か方法思いついたら教えてくださいm(_ _)m

そこを除けば、後はもう煮るなり焼くなりするだけで、声で便利にSalesforceを使えてしまうはずです。
新規リード/今月の営業ランキング/承認待ちのワークフローなどの確認、活動記録、Chatter投稿などできたら便利ですかね?
次はもうちょっと実用的なのを考えたいものですね。
あとは予約したけど音沙汰のないEchoも試してみたいところです。数日前にAlexa for Businessが発表されてSalesforceと公式に連携しちゃうそうですけどね。

参考

32
16
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
16