Twilio x NCMBハンズオン

:::note alert
2023年5月1日を持ちまして、株式会社KDDIウェブコミュニケーションズのTwilioリセール事業が終了したため、本記事に記載されている内容は正確ではないことを予めご了承ください。
:::

# はじめに

本記事は、2019年5月27日に開催された「[Twilio x NCMBハンズオン](https://ncmb.connpass.com/event/130508)」のTwilioのパートで利用したものです。

実施にあたって、すでにTwilioのアカウントを取得していることとします。もしまだ取得していない場合は、[こちら](https://twilio.kddi-web.com/signup/)からオンラインサインアップしてください。

# ハンズオン手順

今回のハンズオンでは、スマホからRestAPI経由でTwilio Studioを呼び出し、呼び出されたフローから電話を架けます。相手が応答するとメッセージを流して、なにかキー入力を行います。入力されたキーは、Twilio Syncを使ってアプリに通知します。

この流れを実装するために、このパートで実施する手順は以下のとおりです。

1. Syncサービスの作成とAPIキー生成
2. Token生成用Functionの生成
3. 応答キーのSync書き込みFunctionの生成
4. 電話番号の購入
5. Twilio Studioのフロー作成

# ハンズオン

それでは早速始めていきましょう。

## Syncサービスの作成とAPIキー生成

### Twilio Syncとは

- [Twilio Sync](https://jp.twilio.com/sync)
- 複数のデバイス間で情報を同期させるためのサービス
- WebSocketやSocket.ioのようなリアルタイム通信プラットフォーム
- Syncは、受け取ったデータをTwilio上に保存してから配信する
- Sync上のデータは、TTLを設定するか、明示的に削除しない限り消えない
- 月10,000アクションまでは無料、それ以降は1000アクション単位で1.5円
- Documents / Lists / Maps / Message Streams の4種類のデータを扱うことが可能

まずは、新しいSyncサービスを作成し、API KeyとAPI Tokenを発行します。

- 管理コンソールのスライドメニューから、DEVELOPER TOOLSの中の**Sync**を選択します。

![上級編Syncサービス.png](https://qiita-image-store.s3.amazonaws.com/0/86046/0d4f05b9-c83f-a149-cf05-f612e545993b.png)

- Syncメニューの中から**Services**を選択します。

![上級編SyncService (1).png](https://qiita-image-store.s3.amazonaws.com/0/86046/a24f55ec-1391-6d57-7bba-745767a65d99.png)

- 赤い**+**アイコンを押して、新しいSyncサービスを作成します。
- **新規Sync Serviceの作成**ダイアログが表示されるので、名前欄に「NCMB」と入力し、**Create**ボタンを押します。

<img width="600" alt="スクリーンショット 2019-05-25 08.40.42.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/a6c3b228-eb45-5ee2-1fe1-3a21364b0095.png">


- **SERVICE SID**（ISから始まる文字列）をメモ帳に保存しておきます。
- Syncメニューの**< Back**をクリックして、Syncサービスの一覧画面に戻ります。
- Syncメニューの**ツール**を選択し、APIキーの一覧が表示されるのを確認します。
- **Create API Key**ボタンを押すか、**+**アイコンを押して、新しいAPI Keyを作成します。
- 名前欄に「NCMB」と入力し、キータイプは「Standard」を選択します。

<img width="947" alt="スクリーンショット 2019-05-25 08.43.50.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/c7654d10-6e03-317c-1c0e-978acc8dff4f.png">

- **APIキーを作成する**ボタンを押します。
- 表示される**SID**（SKから始まる文字列）と、**SECRET**に表示されている文字列の両方をメモ帳に保存します。
- **完了しました!**のチェックボックスにチェックを入れて、**終了**ボタンを押します。

![APIKey.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/d2b08d90-ef72-ad51-b431-9b919262ede0.png)

## Token生成用Functionの生成

上記手順でTwilio SyncのAPIキーを生成しましたが、実際にSyncを利用するためには、作成したAPIキーとSecretをつかって、**Token**を生成する必要があります。Tokenには有効期限があり、期限を超えるとTokenは使えなくなります。
Token自体は文字列で、TwilioにはTokenを作成するためのSDKが用意されています。今回は、Twilio Functionsというサーバーレス実行環境を使って、Tokenを動的に生成するためのプログラムを作ります。

- 管理コンソールから、**Runtime** > **Functions** > **設定**を選択します。
- **Enable ACCOUNT_SID and AUTH_TOKEN**のチェックボックスをONにします。
- **Environmental Variables**の赤い**+**ボタンを３回押して、KEY/VALUE欄を３つ追加します。
- それぞれのKEY/VALUEを以下のように設定します。

|KEY|VALUE|
|:--|:--|
|SYNC_SERVICE_SID|ISから始まるSyncサービスのSID|
|TWILIO_API_KEY|SKから始まるAPIキー|
|TWILIO_API_SECRET|APIキーとセットで設定されたSECRET|

- **Save**ボタンを押して設定を保存します。
- **Runtime**メニューの**Functions** > **Manage**を選択します。
- **Create a Function**ボタンを押すか、もしくは赤い**+**アイコンをクリックして、新しいFunctionを生成します。
- テンプレートの一覧から**Blank**を選択して、**Create**ボタンを押します。
- **FUNCTION NAME**に「SyncToken」と入力し、**PATH**には、「/sync-token」と入力します。

<img width="600" alt="スクリーンショット 2018-07-15 10.25.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/86046/dc7f76d9-30da-959b-ce55-9d16fb56f3a0.png">

- **ACCESS CONTROL**のチェックは外しておきます。
- **CODE**欄に書かれているコードを以下のコードに置き換えます。

```javascript
exports.handler = function(context, event, callback) {
    const ACCOUNT_SID = context.ACCOUNT_SID;
    const SERVICE_SID = context.SYNC_SERVICE_SID;
    const API_KEY = context.TWILIO_API_KEY;
    const API_SECRET = context.TWILIO_API_SECRET;
    const IDENTITY = context.DOMAIN_NAME;
    const AccessToken = Twilio.jwt.AccessToken;
    const SyncGrant = AccessToken.SyncGrant;

    const syncGrant = new SyncGrant({
        serviceSid: SERVICE_SID
    });

    const accessToken = new AccessToken(
        ACCOUNT_SID,
        API_KEY,
        API_SECRET
    );

    accessToken.addGrant(syncGrant);
    accessToken.identity = IDENTITY;
    if (event.callback) {
        let response = new Twilio.Response();
        response.setBody(`${event.callback}(${JSON.stringify({token: accessToken.toJwt()})})`);
        response.appendHeader('Content-Type', 'application/javascript');
        callback(null, response);
    } else {
        callback(null, {
            token: accessToken.toJwt()
        });
    }
};
```

- **Save**ボタンを押して、デプロイされるのを待ちます。
- デプロイが完了したら、**PATH**欄のURLをコピーして、別のブラウザタブで実行してみます。
- JSON形式の長い文字列が表示されたら、トークンの生成は成功です。

## 応答キーのSync書き込みFunctionの生成

次に、電話に応答したあとに押されたキーをSyncに書き込むFunctionを作りましょう。
Syncで共有できるデータには、Documents、Maps、Listsがありますが、今回の目的であれば単純なKey/Valueで良いかと思いますので、Documentsを使うことにします。

### Documentsオブジェクトとは

- 単一のJSON形式オブジェクト
- 1データのサイズは最大16KB
- 単純な値のPUB/SUBに向いている
- 履歴を取るような使い方には向いていない
- JavaScript SDKを使うと、Documentオブジェクトの生成、更新、購読、削除が可能
- サーバーサイドのREST API経由でも管理可能

作成するDocumentの例（Keyが電話番号、押されたキー値がValue）

```json
{
   +819012345678:1
}
```

これから作成するFunctionは、Twilio Studioから呼ばれます。呼ばれたときに、パラメータとして電話番号と押されたキー値を受け取ります。パラメータの名前は以下の通り定義しておきます。

|パラメータ名|内容|
|:--|:--|
|number|電話番号|
|digit|押されたキー値|

- 管理コンソールの**Runtime**メニューの**Functions** > **Manage**を選択します。
- 赤い**+**アイコンをクリックして、新しいFunctionを生成します。
- テンプレートの一覧から**Blank**を選択して、**Create**ボタンを押します。
- **FUNCTION NAME**に「SetDigit」と入力し、**PATH**には、「/set-digit」と入力します。
- **ACCESS CONTROL**のチェックは外しておきます。
- **CODE**欄に書かれているコードを以下のコードに置き換えます。

```javascript
exports.handler = function(context, event, callback) {
    const number = event.number || '';
    const digit = event.digit || '';
    let sync = Runtime.getSync({serviceName: context.SYNC_SERVICE_SID});
    let payload = {};
    payload[number] = digit;

    sync.documents("Digits")
        .update({
            data: payload
        }).then(response => {
            console.log(response)
            callback(null, 'OK')
        }).catch(error => {
            sync.documents.create({
                uniqueName: "Digits",
                data: payload
            }).then(response => {
                console.log(response)
                callback(null, 'OK')
            }).catch(error => {
                console.log(error)
                callback(error)
            })
        })
};
```

- **Save**ボタンを押して、デプロイされるのを待ちます。
- デプロイが完了したら、**PATH**欄のURLをコピーして、メモ帳に保存しておきます。

## 電話番号の購入

では次に、電話をかけるための電話番号を購入します。
今回は050番号を購入します。
すでに電話番号を購入している人は、その番号を使うこともできます。
もしまだ購入していない人は、以下の手順で番号を購入します。

- ブラウザで[ログイン画面](https://jp.twilio.com/login/kddi-web)を表示し、ご自分のIDとパスワードでログインをします。
- 管理コンソールの左側にあるボタンアイコンを押すと、スライドメニューが表示されます。

![002 (1).png](https://qiita-image-store.s3.amazonaws.com/0/86046/e9bd6295-1b6e-9eb7-fdf4-1075b4d610c3.png)

- スライドメニューの一覧から、**電話番号**を選択します。

![005 (1).png](https://qiita-image-store.s3.amazonaws.com/0/86046/7a3e89e8-540a-8c9e-2340-fdcbecdc80d6.png)
- **Phone Numbers**メニューの中の**Buy a Number**を選択します。
- 国のプルダウンから「Japan（+81）」を選択し、**検索**ボタンを押します。

![006.png](https://qiita-image-store.s3.amazonaws.com/0/86046/e3e0c258-960e-f8fa-5301-62c4c28b2834.png)

- 一覧表示されたリストの中から、TYPEがローカルになっている（108円）の番号を一つ選び、**購入**ボタンを押します。
- この番号を購入しますか？というダイアログが出たら、**この番号を購入する。**を押します。
- Congratulationのダイアログが表示されたら、購入完了です。**閉じる**ボタンを押します。
- Phone Numbersの中のManage Numbersを選択し、今購入した電話番号が表示されることを確認します。
- 購入した電話番号はメモ帳にコピーしておいてください。

## Twilio Studioのフロー作成

さて、いよいよ最後の作業となります。
Twilioを使って電話を架けて、相手が応答したらメッセージを流して、キー応答を受け付けます。受け付けたキー情報は、先ほど作成したFunctionに渡していきます。
意外と難しそうに見えますが、Twilio Studioを使うとドラッグアンドドロップでコールフローを作成できます。
今回はすでに作成済みのフローをインポートして再利用したいと思います。

- 管理コンソールの左側のスライドメニューから、Studioを選択します。
- 赤いプラスアイコンをクリックするか、**Create a new flow**を選択して、新しいフローを作成します。
- **FLOW NAME**に「CallPhone」と入力して、**NEXT**ボタンを押します。
- **New Flow**ダイアログが表示されるので、**Import from JSON**を選択して、**Next**ボタンを押します。

![スクリーンショット 2019-01-07 16.57.36.png](https://qiita-image-store.s3.amazonaws.com/0/86046/b70b1156-98b9-cb54-d5b1-3edb8118ba06.png)

- **New Flow**ダイアログが開きますので、以下のJSONを貼り付けます。

```json
{
  "description": "CallPhone",
  "states": [
    {
      "type": "InitialState",
      "name": "Trigger",
      "properties": {
        "offset": {
          "x": 0,
          "y": 0
        },
        "flow_url": "https://webhooks.twilio.com/v1/Accounts/ACxxxx/Flows/FWxxxx"
      },
      "transitions": [
        {
          "event": "incomingMessage",
          "conditions": [],
          "next": null,
          "uuid": "28d3857d-4e53-48d1-9a3b-c91fc8ee3ff9"
        },
        {
          "event": "incomingCall",
          "conditions": [],
          "next": null,
          "uuid": "2d918680-d0bb-4e24-9565-40e45b21990b"
        },
        {
          "event": "incomingRequest",
          "conditions": [],
          "next": "FFde9b38ef0d899aef5b9137de7f9345b1",
          "uuid": "5f50d2ee-7b21-47f4-acb9-f5e4027de178"
        }
      ],
      "sid": "FFe9513435797fbd0ed4d6ca2c40b0d900"
    },
    {
      "sid": "FFde9b38ef0d899aef5b9137de7f9345b1",
      "name": "CallPhone",
      "type": "Dial",
      "properties": {
        "from": "{{flow.channel.address}}",
        "to": "{{contact.channel.address}}",
        "timeout": 60,
        "offset": {
          "x": 26.444444444444457,
          "y": 219.44444444444446
        }
      },
      "transitions": [
        {
          "widgetId": "FFde9b38ef0d899aef5b9137de7f9345b1",
          "uuid": "b5cf69e9-750c-4481-a051-81de390c9c3a",
          "event": "answered",
          "conditions": [],
          "next": "FF48e69d9c562d7965f647c47339359c8a"
        },
        {
          "widgetId": "FFde9b38ef0d899aef5b9137de7f9345b1",
          "uuid": "dfbe008b-6dc0-4b9c-9f2c-1163967d5bfe",
          "event": "answeredByMachine",
          "conditions": [],
          "next": null
        },
        {
          "widgetId": "FFde9b38ef0d899aef5b9137de7f9345b1",
          "uuid": "b515fd2d-b2fd-44a2-8efa-9095c4135cab",
          "event": "busy",
          "conditions": [],
          "next": null
        },
        {
          "widgetId": "FFde9b38ef0d899aef5b9137de7f9345b1",
          "uuid": "4328e132-0cc5-4bb7-87a8-fb0317c7b071",
          "event": "noAnswer",
          "conditions": [],
          "next": null
        },
        {
          "widgetId": "FFde9b38ef0d899aef5b9137de7f9345b1",
          "uuid": "607880f6-3ee7-4c4f-8668-131abf284f15",
          "event": "failed",
          "conditions": [],
          "next": null
        }
      ]
    },
    {
      "sid": "FF48e69d9c562d7965f647c47339359c8a",
      "name": "GatherDigit",
      "type": "Gather",
      "properties": {
        "loop": "3",
        "timeout": 5,
        "stop_gather": true,
        "finish_on_key": "#",
        "offset": {
          "x": -90,
          "y": 480
        },
        "say": "おてすうですが、いずれかの数字キーを押してください。",
        "language": "ja-JP",
        "voice": "Polly.Mizuki",
        "gather_language": "en",
        "play": null,
        "number_of_digits": "1"
      },
      "transitions": [
        {
          "widgetId": "FF48e69d9c562d7965f647c47339359c8a",
          "uuid": "fc2daf3d-ed8b-488a-9d4a-495aa810f6b5",
          "event": "keypress",
          "conditions": [],
          "next": "FF8051dc505207a50adec485f061b56cbe"
        },
        {
          "widgetId": "FF48e69d9c562d7965f647c47339359c8a",
          "uuid": "9c2666ef-e7df-41f5-bf85-2d192320f34e",
          "event": "speech",
          "conditions": [],
          "next": null
        },
        {
          "widgetId": "FF48e69d9c562d7965f647c47339359c8a",
          "uuid": "8068370c-0b32-4119-9a28-780eb2667370",
          "event": "timeout",
          "conditions": [],
          "next": "FF48e69d9c562d7965f647c47339359c8a"
        }
      ]
    },
    {
      "sid": "FF8051dc505207a50adec485f061b56cbe",
      "name": "SetDigit",
      "type": "Function",
      "properties": {
        "offset": {
          "x": -80,
          "y": 730
        },
        "url": "https://xxxx-xxxx-xxxx.twil.io/set-digit",
        "parameters": [
          {
            "key": "number",
            "value": "{{widgets.CallPhone.To}}"
          },
          {
            "key": "digit",
            "value": "{{widgets.GatherDigit.Digits}}"
          }
        ]
      },
      "transitions": [
        {
          "widgetId": "FF8051dc505207a50adec485f061b56cbe",
          "uuid": "6c2d078a-d18e-42fb-b59f-3cbe078fba2b",
          "event": "success",
          "conditions": [],
          "next": null
        },
        {
          "widgetId": "FF8051dc505207a50adec485f061b56cbe",
          "uuid": "2dfcc350-a4ee-41c9-b885-3960d1444100",
          "event": "fail",
          "conditions": [],
          "next": null
        }
      ]
    }
  ]
}
```

- **Next**ボタンを押します。
- 次のようなフローが読み込まれます。

<img width="338" alt="スクリーンショット 2019-05-26 09.41.33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86046/bbbec2db-eb8d-36bf-9b5c-d1c7c52d09da.png">

- **setDigit**ウィジェットを選択します。
- **FUNCTION URL**のプルダウンリストから先程作成した「SetDigit」を選びます。
- **Save**ボタンを押します。
- 画面上部の**Publish**ボタンを押します。

**<重要>**
もし値を変更したときは、**Save**ボタンで変更を保存した後、かならず画面上部の赤い**Publish**ボタンを押してください。これを忘れると設定は反映しません。

このStudioフローをRestAPIで呼ぶためには以下の条件が必要です。

- Studioフローの**Trigger**ウィジェットを選択すると、RestAPI用のURLが表示されます。
- POSTメソッドで呼び出してください。
- Basic認証が必要です（UsernameとPasswordは、SyncのAPIキーとSecretがそのまま利用できます）。
- 発信者番号と発信元番号は、それぞれ**To**と**From**パラメータで指定が必要です。
- Context-Typeは、**application/x-www-form-urlencoded**で送ってください。

Curlでの呼び出し例

```sh
curl -X POST \
  https://studio.twilio.com/v1/Flows/FWxxxxxx/Executions \
  -H 'Authorization: Basic ,Basic ' \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -d 'To=%2B8180XXXXXXXX&From=%2B8150XXXXXXXX'
```

---
### Twilio（トゥイリオ）とは

https://twilio.kddi-web.com
Twilioは音声通話、メッセージング（SMS/チャット）、ビデオなどの 様々なコミュニケーション手段をアプリケーションやビジネスへ容易に組み込むことのできるクラウドAPIサービスです。初期費用不要な従量課金制で、各種開発言語に対応しているため、多くのハッカソンイベントやスタートアップなどにも、ご利用いただいております。