Amazon ConnectのエージェントイベントをSalesforceで処理する

> この記事は旧バージョンの Amazon Connect Salesforce CTI アダプタに対する記事です。
最新の v5 系の CTI アダプタを使用している場合、GUI ベースの CTI Script を使用してイベントを処理することができます。詳細は公式のドキュメントをご確認ください。

Amazon ConnectではSalesforce用のOpen CTIアダプタが提供されており、最近では[SSOやオムニチャネル連動など便利な機能](https://aws.amazon.com/jp/blogs/news/building-an-automated-ai-experience-with-amazon-connect-and-salesforce-service-cloud/)が増えてきました。しかし、着信時や切電時のようなテレフォニー側のイベントをSalesforceで処理するには少々工夫が必要です。簡単に思いつく方法としては以下があります。

* [ソフトフォンレイアウト](https://help.salesforce.com/articleView?id=cti_admin_phonelayouts.htm&type=5)にVisualforceページを指定してカスタムロジックを実行する
  * 着信時だけであればこれが一番簡易ですが、一度余計な画面が挟まってしまいます。
* Lightningコンポーネントで[Amazon Connect Streams API](https://github.com/aws/amazon-connect-streams)を利用する
 * [Locker Service](https://developer.salesforce.com/docs/atlas.ja-jp.lightning.meta/lightning/security_code.htm)というSalesforce独自のセキュリティルールのため、スクリプトを直接読み込むことができません…😭
* Open CTIアダプタそのものを作り直す
 * VisualforceページであればStreams APIが読み込めますが、アダプタを作り直すのは単純に手間がかかりますし、標準で提供されているアダプタの機能はそのまま利用したいところです。

そこで、やや遠回りではありますが、Kinesisストリームに流れるAmazon ConnectのエージェントイベントをトリガにLambdaを起動してSalesforceに通知するサンプルを紹介します。

## 連携イメージ
<img width="800" alt="Overview" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184624/ac193539-43dd-1d4c-8779-c5aa78efac49.png">


## 設定手順
### Amazon Connectでエージェントイベントストリームを有効化する
データストリーミングを有効化し、エージェントイベントのセクションでKinesisストリームを選択します。ない場合は新規作成します。
<img width="800" alt="amazon connect agent stream setting" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184624/4c3491c7-8a4a-65f1-c2ce-ea1b7151c2bd.png">


### Salesforceでプラットフォームイベントを設定する
プラットフォームイベントは、Salesforceでイベント駆動型のシステム連携を実現するための仕組みです。[設定] - [プラットフォームイベント]からプラットフォームイベントを新規作成します。ここでは、ContactId(呼のId)、エージェントのユーザ名、呼のステータスを渡すための項目を作成してみます。

<img width="800" alt="platform event setting" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184624/935379c1-1513-a581-ee28-42b2d4f75c80.png">

### プラットフォームイベントをPublishするLambdaを作成する
サンプルのLambdaプログラムは以下の通りです。全量は[こちらのリポジトリ](https://github.com/shunkosa/amazonconnect-kinesis-salesforce-integration-example)をどうぞ。エージェントイベントの構造は[公式のドキュメント](https://docs.aws.amazon.com/ja_jp/connect/latest/adminguide/agent-event-streams.html)を参照してください。

```handler.ts
import * as jsforce from 'jsforce';
import 'source-map-support/register';
import { Buffer } from 'buffer';
import { KinesisStreamEvent, KinesisStreamHandler } from 'aws-lambda';

export const index :KinesisStreamHandler = async (event :KinesisStreamEvent): Promise<any> => {
    interface AgentEvent {
        ContactId__c: String,
        Username__c: String,
        ContactState__c: String,
    }
    let sfEventList: AgentEvent[] = [];
    for(const record of event.Records){
        const base64Record = record.kinesis.data;
        const decodedRecord = JSON.parse(Buffer.from(base64Record, 'base64').toString());
        if(decodedRecord.EventType != "HEART_BEAT" && decodedRecord.CurrentAgentSnapshot.Contacts.length > 0){
            sfEventList.push({
                ContactId__c : decodedRecord.CurrentAgentSnapshot.Configuration.ContactId,
                Username__c : decodedRecord.CurrentAgentSnapshot.Contacts[0].Username,
                ContactState__c : decodedRecord.CurrentAgentSnapshot.Contacts[0].State
            });
        }
    }

    const conn = new jsforce.Connection({
        loginUrl: process.env.LOGIN_URL
    });
    await conn.login(process.env.SALESFORCE_USERNAME, process.env.SALESFORCE_PASSWORD);
    conn.sobject("AmazonConnectAgentEvent__e").insert(sfEventList);

    return {
        eventRecords : sfEventList
    };
}
```
* KinesisストリームのイベントデータはBase64のためデコードが必要です。
* ``EventType``が``HEART_BEAT``のものはシステムイベントなので除外すると良いでしょう。
* コールフローで設定した問い合わせ属性はエージェントイベントには含まれないので、[APIで取得する](https://docs.aws.amazon.com/connect/latest/APIReference/API_GetContactAttributes.html)必要があります。
* Winter' 19以降に作成するプラットフォームイベントの種別は自動的に大量(High Volume)になりますが、コンタクトセンターの規模と[プラットフォームイベントの制約](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_event_limits.htm)を鑑みて必要な場合のみPublishしましょう。

### SalesforceでプラットフォームイベントをSubscribeする
UIで処理が必要な場合は[バックグラウンドのユーティリティ項目](https://developer.salesforce.com/docs/atlas.ja-jp.220.0.api_console.meta/api_console/sforce_api_console_js_lightning_background_utility_item.htm)を、そうでない場合は[Apexトリガ](https://developer.salesforce.com/docs/atlas.ja-jp.platform_events.meta/platform_events/platform_events_subscribe_apex.htm)や[プロセスビルダー](https://developer.salesforce.com/docs/atlas.ja-jp.platform_events.meta/platform_events/platform_events_subscribe_process.htm)を用いてSubscribeすると良いでしょう。以下はバックグラウンドのユーティリティ項目のサンプルです。

```AgentEventSubscriber.cmp
<aura:component implements="lightning:backgroundUtilityItem">
    <lightning:empApi aura:id="empApi"/>
    <aura:handler name="init" value="{!this}" action="{!c.doInit}" />
</aura:component>
```

```AgentEventSubscriberController.js
({
    doInit : function(component, event, helper) {
        const empApi = component.find("empApi");
        const channel = "/event/AmazonConnectAgentEvent__e";
        const replayId = -1; //登録後に発生したすべての新規イベントを受信

        // イベント受信時の処理
        const onMessageCallback = (message) => {
            console.log("Event received : " + JSON.stringify(message));
            //ここに例えば一致するログインユーザに対してタブを制御する処理を入れるなど
        };

        // Subscribeの開始
        empApi.subscribe(channel, replayId, onMessageCallback).then((value) => {
            console.log("Subscribed to channel " + channel);
        });    

        // エラー処理
        empApi.onError((message) => {
            console.log("Received error ", message);
        });  
    }
 })
```

messageの中身は以下のようになります。

```json
{
    "data": {
        "schema": "HcRzaHqAkOE7bF_o5gQjiA",
        "payload": {
            "ContactId__c": "14f3a899-da72-4d39-a39f-84b6adae687b",
            "ContactState__c": "CONNECTING",
            "CreatedById": "0052v00000W0wg4AAB",
            "CreatedDate": "2019-06-30T03:43:55Z",
            "Username__c": "shun.kosaka"
        },
        "event": {
            "replayId": 4711
        }
    },
    "channel": "/event/AmazonConnectAgentEvent__e"
}
```

最後に、アプリケーションの設定で作成したユーティリティバーを含めれば完了です。
<img width="788" alt="application setting" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184624/6df75180-471f-ead7-e314-62069b136a6d.png">

## おわりに
Amazon ConnectのエージェントイベントストリームはWFMソリューションとの連携が大半の用途だとは思いますが、Salesforceでイベントを処理することで電話応対時のUXの向上やデータの統合(レポーティングに関してはSalesforceで行うのが良いのかという議論もありますが)などが実現可能です。より簡易に統合できるような、今後のOpen CTIアダプタのアップデートに期待したいところです。