Quip Live Appsの何かを書くと予告していたのですが、すみません。予告詐欺です。
Google Homeから音声でSalesforceを操作するGoogle Homeアクションを作ってみることになりました。
今回作るアクションのシナリオ
- Salesforceのアカウントと紐付ける
- ユーザ「Salesforceアプリにつないで」
- ボット「こんにちは。Salesforceアプリです。」
- ユーザ「今日の予定は?」
- ボット「今日の予定はXXXです。」
みたいなのを作ろうと思います。
内容はとりあえず連携を試すためのものではあります。
しかし、実はGoogle Homeは個人用Gmailアカウントとリンクしており、Googleカレンダー連携機能が役に立たないため、Salesforceで予定が確認できると個人的に便利なのではないかと期待もしています。
動くものをすぐに見てみたい方は下の方に動画がありますので、そちらをご覧ください。
Account Linkingの設定
Salesforceと連携するにはどうしたってユーザ認証する必要があります。
Google HomeにはAccount Linkingsという仕組みがあるのでこれを利用します。
Salesforce側の設定
以下のような設定で接続アプリケーションを作成します。
- コールバックURL:
https://oauth-redirect.googleusercontent.com/r/<google developer project ID>
- 選択したOAuth範囲: full(テストなので)、refresh_token
Actions Consoleの設定
Dialogflowでシミュレータが動くところまでやっておきます。
Overviewから(5)Account Linkingを選び[Add]をクリック
順番に入力していきます。
- Client ID: 上で作成した接続アプリケーションのコンシューマ鍵(翻訳おかしい)
- Client secret: 上で作成した接続アプリケーションのコンシューマの秘密(翻訳おかしい)
- Authorization URL: https://login.salesforce.com/services/oauth2/authorize
- Token URL: https://login.salesforce.com/services/oauth2/token
- Scopes:
- refresh_token
- full
- Testing instructions: レビュー用の項目なので適当に入力
Dialogflowを開き、[Sign in required for welcome intent]にチェック
認証連携の確認
シミュレータで確認すると、「まだリンクされていません」と表示されます。
(この状態でGoogle Home実機に話しかけても同じことになります。)
[DEBUG]タブにURLが表示されるので、そのURLをブラウザで開きます。
(Google Homeアプリからもできるらしいのですが、見当たりませんでした。開発版ではできないのだろうか?)
Salesforceへのログイン画面が表示されるのでログイン。
シミュレータに戻って再度アプリを呼び出すと、今度はちゃんと動き、accessTokenが取れていることがわかります。
(この状態でGoogle Home実機に話しかけても同じことになります。)
アプリの実装
アカウント連携ができたので、アプリを実装していきます。
今回はDialogflowとCloud Functions for Firebaseを使っていきます。
Entities
エンティティ名 | 内容 |
---|---|
end | 終了コマンド |
object | Salesforceのオブジェクト名 |
what-to-know | 「教えて」や「知りたい」という単語 |
end
object
what-to-know
正直、どんな粒度でどのように定義すればいいかよくわかっていません。
Intents
インテント名 | 内容 |
---|---|
Default Welcome Intent | アプリを起動したとき |
Default Fallback Intent | わからないとき |
GetRecordsByDate | メイン 日付とオブジェクト名でSalesforceのレコードを取得します。 |
End | 終了するとき |
Default Welcome Intent
Text Responseを変更しただけでそれ以外はデフォルトのままです。
Default Fallback Intent
こちらは後でFulfillmentから呼ぶためにEvent名をつけました。
あとはText ResponseをGoogle Homeらしく変更しただけです。
GetRecordsByDate
メインの機能です。
「@sys.date
の@object
を@want-to-know
」のように定義しています。
そうすると、あとでFulfillmentでパラメータとして受け取れます。
@sys.date
は定義済みの日付に対応するエンティティです。「今日」「4日前」「一週間後」「来週の月曜」なども解釈してくれるのでなかなか便利です。
定義済みのエンティティはSystem Entitiesと言って他にもいろいろあります。
最後に[Fulfillment]>[Use webhook]にチェックを入れます。これでこのintentのときはFulfillmentで処理をするようになります。
End
@end
エンティティのときに呼ばれるようにします。
[Google Assistant]>[End conversation]にチェックを入れると、このインテントが処理されると会話を終了するようになります。
Fulfillment
楽なのでInline EditorでCloud Functions for Firebaseを使います。
今回はSalesforceのREST APIを呼びます。無料プランだとGoogle外のドメインにリクエストを投げられませんので、従量課金のBlazeプランに変更する必要があります(Firebaseの価格表)。
package.json
いつもお世話になっているJSforceを追加します。
"jsforce": "^1.8.0"
index.js
以下のようにしました。
基本的にはInline Editorを有効にしたときに生成されるデフォルトコードをベースに修正しています。Dialogflow's V2 APIはまだactions-on-google
のnpmが対応していなそうだったので、v2のコードはごっそり削除しています。
'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アプリ」に変更しています。
Google Homeで動かす
実機だとこんな風に動きます。動画です。画像をクリックするとYouTubeに飛びます。
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と公式に連携しちゃうそうですけどね。
参考
-
Google Homeアプリをリリースしてみた(実装編) - Qiita
- 見ていただいてわかる通り記事構成含めて大変参考にさせていただきました。