Twilio Syncプログラミングサンプル

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

# はじめに

本記事は、Twilio Syncを使うプログラミングサンプルとなります。
複数のブラウザ（PC、スマホ等）で同期を取りながら、いずれかの端末の変更結果を他の端末にPushします。

<img width="504" alt="スクリーンショット 2018-11-04 09.44.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/86046/dbb3da31-0d26-2c3f-a18e-efe1475d61d6.png">

## 準備

- Twilioアカウント（トライアルアカウントでも可能です）
- ブラウザ（スマホ、PC）

## Twilio Sync

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

---
### Documentsオブジェクト

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

例：Documentsオブジェクトの生成

```lang:JavaScript
client.document('user_prefs').then(function(doc) {
  doc.set({
    foregroundColor: "#ffff00",
    backgroundColor: "#ff0000"
  });
});
```

すでに、user_prefsオブジェクトが存在する場合は、そのオブジェクトが上書きされます。

例：Documentsオブジェクトの更新

```lang:JavaScript
client.document('user_prefs').then(function(doc) {
  doc.update({foregroundColor: "#ff0000"});
});
```

user_prefsに、別のKey/Value（例えば、backgroundColor）があった場合、その値は更新されません。

例:Documentsオブジェクトの読み込み

```lang:JavaScript
client.document('user_prefs').then(function(doc) {
  console.log(doc.value);
});
```

例:Documentsオブジェクトの更新を購読する

```lang:JavaScript
syncClient.document("user_prefs").then(function(doc) {
  doc.on("updated",function(data) {
    console.log(data);
  });
});
```

例:Documentsオブジェクトの削除

```lang:JavaScript
syncClient.document("user_prefs").then(function(doc) {
  doc.removeDocument().then(function() {
    console.log('Document removed.');
  });
});
```

## ハンズオン

Documentsオブジェクトを使ったデバイス同期

#### シナリオ

SyncのDocumentsオブジェクトを利用して、複数ブラウザで同期を行うサイトを作成してみます。
SyncのJavaScript Clientを利用します。

### 手順1. Syncサービスの作成と、API KeyとAPI Tokenの発行

まずは、新しい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 Servceに名前をつけます。**ダイアログが表示されるので、名前欄に「SyncBrowser」と入力し、**Create**ボタンを押します。

<img width="541" alt="スクリーンショット 2018-07-15 09.12.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/86046/7121d1de-e1dc-af1e-6a0b-34af028c315b.png">

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

<img width="923" alt="スクリーンショット 2018-07-15 09.19.07.png" src="https://qiita-image-store.s3.amazonaws.com/0/86046/f3e913cb-97be-1bda-1125-518cff247184.png">

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

![上級編SyncApiKey.png](https://qiita-image-store.s3.amazonaws.com/0/86046/6b53c312-88f2-57a2-94e9-1a1690d90f6a.png)

### 手順2. アクセストークン発行用Functionの作成

Syncを利用するためには、先程作成したAPIキーを使って、アクセストークンを動的に生成する必要があります。
今回は、Functionsを使って、Syncサービスのアクセストークンを生成してみましょう。

- 管理コンソールから、**Runtime** > **Functions** > **Configure**を選択します。
- **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**を選択します。
- 赤い**+**アイコンをクリックして、新しい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**欄に書かれているコードを以下のコードに置き換えます。

```lang:Node.js
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;
    callback(null, {
        token: accessToken.toJwt()
    });
};
```

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

### 手順3. HTMLファイルのアップロード

では次に、ブラウザに表示するHTMLファイルを準備します。

- 以下のURLからZIPファイルをダウンロードします。
- ```https://sapphire-donkey-2666.twil.io/assets/SyncBrowser.zip```
- ダウンロードしたZIPファイルを解凍し、２つのファイルがあることを確認します。
 - index.html
 - index.js
- 管理コンソールの**Runtime** > **Assets**を選択します。
- **+**アイコンを押して、index.htmlを選択します。**MAKE PRIVATE**のチェックは外しておきます。

<img width="541" alt="スクリーンショット 2018-07-15 11.15.41.png" src="https://qiita-image-store.s3.amazonaws.com/0/86046/e525c5c5-b663-1256-4763-39735bdfedbb.png">

- **Upload**ボタンを押して、アップロードを完了します（デプロイが完了するまで待ちます）。
- 同様に、index.jsもアップロードします。

```lang:index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Sync Browser</title>
    <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/css/bootstrap.min.css" rel="stylesheet">
    <!-- jQuery読み込み -->
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
    <!-- BootstrapのJS読み込み -->
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.2/js/bootstrap.min.js"></script>
    <script src="https://media.twiliocdn.com/sdk/js/sync/releases/0.8.4/twilio-sync.min.js"></script>
    <script src="./index.js?version=1.04"></script>
  </head>
  <body>
    <h3>押されたボタンの状況を同期します</h3>
    <button id="btn1" type="button" class="btn btn-primary">ボタン1</button>
    <button id="btn2" type="button" class="btn btn-warning">ボタン2</button>
    <button id="btn3" type="button" class="btn btn-danger">ボタン3</button>
    <p>
        <div  id="result"></div>
    </p>
  </body>
</html>
```

```lang:index.js
let syncDoc, syncClient;

$(function() {
    
    // Twilio Sync Tokenの取得
    $.getJSON('../sync-token', function (tokenResponse) {
        // Sync Clientの初期化
        syncClient = new Twilio.Sync.Client(tokenResponse.token, { logLevel: 'info' });
        console.log(tokenResponse.token);

        syncClient.document('SyncBrowser')
        .then(function(doc) {
            // グローバル変数に格納
            syncDoc = doc;

            // Syncからメッセージが届いた場合のイベント
            syncDoc.on('updated', event => {
                text = event.value.text;
                console.log(`Sync updated. ${text}`);
                // 画面を更新
                $('#result').html(text);
            });
        })
        .catch(function(error) {
            console.log(error);
        });

        // Token Expire対策
        syncClient.on('tokenAboutToExpire', () => {
            $.getJSON('../sync-token', function (tokenResponse) {
                syncClient.updateToken(tokenResponse.token);
                console.log('Sync token updated.');
            });
        });
    });

    $('#btn1').on('click', function() {
        setSyncDoc("ボタン１が押されました");
    });
    $('#btn2').on('click', function() {
        setSyncDoc("ボタン２が押されました");
    });
    $('#btn3').on('click', function() {
        setSyncDoc("ボタン３が押されました");
    });

    function setSyncDoc(text) {
        const value = {
            text: text,
        };
        syncDoc.set(value);
        console.log('Sync publish.');
    };
});
```

**<重要>** index.jsのように、Assetsに格納されたコンテンツ（今回の場合は、index.html）から参照されるコンテンツだけを更新してAssetsにアップロードし直しても、キャッシュが効いてしまうので新しいコンテンツが反映されません。そのため、今回の例のように、呼び出すコンテンツに「?ver=1.xx」のようにバージョンを振るなどして、index.htmlとindex.jsの両方をアップロードするとキャッシュされないようになります。

### 手順5. テスト

- 管理コンソールのAssestの一覧画面から、アップロードしたindex.htmlのURLをコピーします。
- 新しいブラウザタブを開き、今のURLを実行します。
- ボタンを押すと、メッセージが表示されることを確認します。
- さらに複数のタブで上記URLを開き、どれかの画面でボタンを押すと、他のタブにも反映されることを確認します。