Amazon Echo から Salesforce のレポートを呼び出してみた

おめでとうございます、かしゆかの誕生日です、こんにちは。

社内でも見られてしまうものとしては、冒頭の文章は失敗でしかない気もしますが、気にしないで Amazon echo で遊んでみた記録を残しておきます。

# 前提条件・必要なもの
1. Salesforce アカウント (Developer Editionで良いでしょう。本番で試すのはDEのあとでどうぞ)
2. Amazon Echo (実機で試す場合。なくても、テストだけはできます)
3. Amazon Developer のアカウント (Alexa で登録したメアドと同じアカウント)
4. AWSアカウント (lambda を利用する場合)

# 今回のシーケンス
アーキテクチャ図を書いたらシンプルすぎてかわいそうになったので、動的シーケンスっぽい図にしてみました。
![Screen Shot 2017-12-21 at 15.53.06.png](https://qiita-image-store.s3.amazonaws.com/0/37263/b39562ca-8f21-69c4-9440-aba7625461e3.png)

大したことはやっていなくて、Alexa Skill Kit でインテントを取得して、lambda上の functionをコールさせています。
lambdaからは、Salesforce へ RESTコールして Apexを呼び出します。Apex はレポートAPIを利用して、商談の売上データをもとに集計した、表形式の金額のサマリー情報を取得し、lambda 側へ返却します。
lambdaはEchoで読み上げるための文章を作成して、Alexa Skill Kit へ戻すという流れです。

lambda部分は、Herokuのアプリを実行させることもできますが、なんとなくlambdaをちゃんと使ってみたかったのと、一般的っぽいのでそちらを採用しました。今度はHerokuでも試してみます。

# 開発のおおまかな手順
では、実際に Alexa Skill Kit や lambdaでの実装手順はどうなるかというと、次の２つで済むケースが多いでしょう。今回は、レポートの集計データを利用したかったので、Apexの開発も行いましたが、Salesforce側での開発が必要なく、RESTで取得できるものであれば、Salesforce側は特にいじる必要はないです。Chatter へ投稿させる程度なら、Chatter APIコールすればいいですしね。

1. Developer Console で Alexa Skill Kit を完全に準備すること
2. lambda か外部の Web上で稼働するアプリを開発し、Alexa Skill からそのアプリへ連携させること

では、具体的な開発の流れは次の手順は、というと次のとおりになります。今回はApex開発がありましたので、2. にはその手順が含まれています。

1. Developer Console で Alexa Skill Kitの基本情報を準備する
2. Salesforce 上で、レポートの集計データを取得する Apex class を開発し、REST で公開する
3. lambda または Web上で稼働する Alexa用アプリを開発する
4. Developer Console 上の Alexa Skill Kitからアプリを連携
5. Developer Console 上で Alexa Skill Kitのアプリをテスト
6. Developer Console で、βテストに必要な次の項目をすべて埋める
    - 公開情報
    - プライバシーとコンプライアンス
7. Skill Beta Testing で、Amazon Echo とひも付けられたメールアドレスを、テスターとして追加し、「アクティブ」化させる

細かい手順については、たくさんの公開された情報があるので、そちらを参照していただければと思いますが、とにかく、Amazon Echo の実機でテストを行うには、Alexa Skill Kit のすべての項目をうめて、申請直前までできあがっている必要があります。

## 各手順での注意・留意事項

### Developer Console で Alexa Skill を作成する場合の注意事項

先にも述べたとおり、実機でテストを行うためには、すべての情報を準備する必要があります。次の各項目すべての設定を完了させると、初めて、Echo 実機でテストをする準備ができあがります。地味に厄介なのは、アイコンがないといけない点です。108x108 と 512x512 のアイコン画像が必要なので、がんばって準備しましょう。

1. Skill Information (スキル情報)
2. Interaction Model (対話モデル)
3. Configuration (設定)
4. Test (テスト)
5. Publishing Information (108x108 および 512x512 のアイコン・ロゴが必須です) (公開情報)
6. Privacy & Compliance (プライバシーとコンプライアンス)

### lambda または Web上で稼働するアプリを開発する際の注意事項

1. Node.js で作る場合には非同期に注意
2. Account Linking で Salesforce との OAuth をさせるのは、残念ながら、現時点では難しいようです。そのため、[JWTフロー](https://qiita.com/stomita/items/4542ce1b48e5fa849ef1)で開発することをおすすめします。なお、リンク先の Expire の指定方法は、今では仕様が変わってしまったようで動きません(stomita さんにはお伝え済み)。下のサンプルコードを参照ください。秒指定になって、整数型になりました。
3. Alexa へのレスポンス返却には、this.emit などを利用するが、「this」が示すとおり、Node.js でコードブロックの入れ子の深さが変わる場合には、”const self = this” などで、退避しておいて、”self.emit(“:tell”, “メッセージ”);” などで返却する必要がある。
4. 自由文章を入力して、それを引数とすることは現時点では、ほぼ無理そう。そのため、メッセージをもらって、その内容を chatter へ投稿するのは Alexaではできなさそう。Googleアシスタントなら行けそうなので、今度はそちらにチャレンジ。

```javascript:index.js
'use strict'

const Alexa = require('alexa-sdk');
const fs = require('fs');
const jwt = require('jsonwebtoken');
const request = require('request');
const jsforce = require('jsforce');

const APP_ID = 'amzn1.ask.skill.xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'; // undefined でも良さげ

const TOKEN_ENDPOINT_URL = 'https://login.salesforce.com/services/oauth2/token'; // mydomain を使っているなら、そっちにしましょう
const ISSUER = '3MVG9ZL0pp(中略).ZHga(中略)nhqW1'; // 接続アプリのコンシューマ鍵（client_id)
const AUDIENCE = 'https://login.salesforce.com'; // 固定
const REPORT_ID = '00O0I00000A8XxXXxXXX'; // 取得するレポートのID
const cert = fs.readFileSync('./ssl/myapp.pem'); // 秘密鍵の読み込み

// JWTに記載されるメッセージの内容
const claim = {
  iss: ISSUER,
  aud: AUDIENCE,
  sub: 'xxxxxx@example.com', // 接続するSalesforceのユーザアカウント名(メアド)
  exp: Math.floor(Date.now()/1000) + 3 * 60 //現在時刻から3分間のみ有効 (以前からの仕様変更部分)
};

// JWTの生成と署名
const token = jwt.sign(claim, cert, { algorithm: 'RS256'});

function handleGetReportSummary(intent, response, callback) {
  // JWT Bearer Token フローによるアクセストークンのリクエスト
  request({
    method: 'POST',
    url: TOKEN_ENDPOINT_URL,
    form: {
      grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
      assertion: token
    }
  }, function(err, response, body) {
    if (err) {
      return console.error("request エラー" + err);
    }
    const ret = JSON.parse(body);
    const conn = new jsforce.Connection({
      accessToken: ret.access_token,
      instanceUrl: ret.instance_url
    });
    const id = { reportId: REPORT_ID };
  // apex で作成した REST をコール
    conn.apex.post("/summary/", id, function(err, result) {
      if (err) { return console.error(err); }
      callback(err, result);
    });
  });
};

const handlers = {
  'LaunchRequest': function () {
    this.emit(':ask', 'chatter へ投稿するメッセージをお伝え下さい');
  },
  'SubmitChatter': function (intent, session, response) {
    const self = this;
    handleGetReportSummary(intent, response, function(err, result) {
      console.log("result  : " + result);
      self.emit(':tell', '今期の売上は' + result + '円です');
    });
  },
  'AMAZON.HelpIntent': function () {
    this.emit(':ask', '現在の売上を報告します');
  },
  'AMAZON.CancelIntent': function () {
    this.emit(':tell', 'またご利用ください');
  },
  'AMAZON.StopIntent': function () {
    this.emit(':tell', 'またご利用ください');
  },
  'Unhandled': function () {
    this.emit(':tell', '未対応です');
  },
};

exports.handler = function (event, context) {
  const alexa = Alexa.handler(event, context);
  alexa.APP_ID = APP_ID;
  // To enable string internationalization (i18n) features, set a resources object.
  alexa.registerHandlers(handlers);
  alexa.execute();
};
```
### レポートの結果を取得するにはApexを叩くしかない

Salesforceでは、レポートのサマリなど情報を取得するには、Apex の Classでアクセスするしか方法がなさそうです。したがって、外部システムからレポートの結果を取得するためには、Apex で Classを開発し、`@RestResource()` と `@HttpPost`アノテーションを使って、外部からREST apiを叩けるように Classを公開する必要があります。今回、表形式で、売上の集計値(合計)をサマリーとしてもつレポートから、集計値を取り出すサンプルを作りましたので、参考になさってください。

レポート大変すぎる。

```java
:summary.apxc
@RestResource(urlMapping='/summary/*')
global with sharing class summary {

  // 現時点では POST のみを受け入れ
  // 引数は JSON形式で、"reportId" に、レポートのIDを指定する必要があります
    @HttpPost
    global static Integer doPost(String reportId) {
		Reports.ReportResults results = Reports.ReportManager.runReport(reportId, true);
		MAP<String,Reports.ReportFact> summary = results.getFactMap();
		LIST<Reports.SummaryValue> aggr = summary.get('T!T').getAggregates();

        return Integer.valueOf(aggr[0].getValue());        
    }
}
```

何やってるか、ものすごい難しいです。検証の結果、こうなりました orz

参考情報

1. [「OAuth2 JWT Bearer Token フローを使ってSalesforceへアクセスする」]( https://qiita.com/stomita/items/4542ce1b48e5fa849ef1)
2. [「Alexa Skills Kit for Node.js はじめの一歩」](https://dev.classmethod.jp/cloud/alexa-skills-kit-for-node-js-first/)
3. [「開発中のAlexaスキルを実機テストする方法」](https://qiita.com/hayao_k/items/a13b0c9327c49934eda3)
4. [「レポートデータの取得」@ Apex開発者ガイド](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_analytics_report_data.htm)