Edited at

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

More than 1 year has passed since last update.

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と公式に連携しちゃうそうですけどね。


参考