Twilioで匿名通話機能を実装してみた(Twilio Functions, TwiML Bins, TwiML Apps)

# はじめに
アプリ内で匿名通話を行うような機能を実装したので、導入手順(主にバックエンド)をまとめました。

# 背景
お互いの電話番号を知らせずに通話する(匿名通話)という要件があったので、Twilioで050番号を購入して自動転送させるという仕組みをつくりました。

Twilioを使うと、匿名通話のようなプライバシーを守るための機能だけではなく、自動応答(音声)などの仕組みも簡単に導入することができます。
例えば、食べログの電話予約で050番号にかけると、店舗側で受話器をとったときに「食べログからの電話です」という音声が流れるようなのですが、このような機能もTwilioで実装することができます。

また、ユーザAの端末からTwilioの050番号にそのまま架電することもできる(回線交換方式)のですが、以下の問題があったので、パケット交換方式(VoIP)による通話機能を実装することにしました。

- ユーザAに発信料、Twilioアカウントに着信料が発生する
- 0120番号に切り替えても、Twilioアカウントへの着信料が発生する
- ユーザAの発信は通話アプリから行うため(作成アプリには通話アプリへのリンクを置くだけ)、通話後に作成アプリへ戻る手間が発生する

![スクリーンショット 2022-02-26 10.39.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/7d92d38a-84dc-b6d8-7d09-47db5a3e6d68.png)

# 実装
TwilioからユーザBに転送を行うためには、転送指示(転送元、転送先の番号)をTwilioに与える必要があります。
そのため、ユーザAからTwilioに架電したときに、どっかのサーバにフックして転送の指示内容を取得する仕組みをつくってあげます。

フック先のサーバを自分で用意してもいいのですが、幸いなことにTwilioでは素早く簡単にサーバを構築できるサーバーレス環境が準備されています。

サーバレスでの実装にはTwilio FunctionsとTwiML Binsを使用する方法があるので、それぞれ紹介します。
用語については以下をご参照ください。

- TwiML：Twilioのマークアップ言語。記述内容の処理(今回の場合は転送処理)をTwilioが実行する。
- TwiML Apps：クライアントの接続先となるTwilio上のアプリケーション。接続にはアクセストークンが必要。
- Twilio Functions：Node.jsで動作するサーバレスの実行環境。今回はクライアントからのアクセストークン取得とTwiML AppsからのTwiML取得(パターン1)に使用。 
- TwiML Bins：自分で記述したTwiMLを配置、利用できる機能。TwiML取得(パターン2)に使用。

※Twilioアカウントと電話番号を取得している前提で紹介していきます。

## パターン1(Twilio Functions)
こちらのパターンでは、クライアントからのアクセストークン取得とTwiML AppsからのTwiML取得のスクリプトをそれぞれ記述して、Twilio Functionsに配置します。

### 構成図
こちらが構成図およびVoIP接続のフローです。
![スクリーンショット 2022-02-26 11.28.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/1ab35b42-b84c-fcc5-5e26-40292574431d.png)

### 手順
以下が導入手順です。

#### TwiML Appsの作成
`Voice > Manage > TwiML apps`ページの`Create new TwiML App`からTwiML Appsを新しく作成します。
![スクリーンショット 2022-02-26 11.41.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/19ad092e-5c2e-01ed-cd16-827ac81ba7bf.png)

#### Twilio Functionsの作成
`Functions > Functions(classic)　> list`からFunctionsを作成します。

以下がアクセストークン取得のFunctions(CallAccessToken)を作成した画面です。
PropertiesのPATHがクライアント側でトークンをフェッチする際のパスとなります。

![スクリーンショット 2022-02-26 11.48.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/03c92446-0d20-1fe7-08a5-ceb6e2c5823c.png)

アクセストークンの生成には、TwilioのアカウントSID、APIキー、APIシークレット、TwiML AppsのSIDが必要です。
これらの値は`Functions > Functions(classic) > Configure`で環境変数として保持します。

```js
exports.handler = function(context, event, callback) {
	const AccessToken = require('twilio').jwt.AccessToken;
    const VoiceGrant = AccessToken.VoiceGrant;
    
    // Used when generating any kind of tokens
    const twilioAccountSid = context.TWILIO_ACCOUNT_SID;
    const twilioApiKey = context.TWILIO_API_KEY;
    const twilioApiSecret = context.TWILIO_API_SECRET;
    
    // Used specifically for creating Voice tokens
    const outgoingApplicationSid = context.TWIML_APP_SID;
    const identity = 'user';
    
    // Create a "grant" which enables a client to use Voice as a given user
    const voiceGrant = new VoiceGrant({
      outgoingApplicationSid: outgoingApplicationSid,
      incomingAllow: true, // Optional: add to allow incoming calls
    });
    
    // Create an access token which we will sign and return to the client,
    // containing the grant we just created
    const token = new AccessToken(
      twilioAccountSid,
      twilioApiKey,
      twilioApiSecret,
      {identity: identity}
    );
    token.addGrant(voiceGrant);
    
    // Serialize the token to a JWT string
	callback(null, token.toJwt());
};
```
こちらが自動転送のTwiMLを返すFunctionsです。
クライアントから渡した転送先や転送元(Twilio)の電話番号パラメータ`To` `Twilio`をもとに、TwiMLを生成します。

```js
exports.handler = function(context, event, callback) {
    // 転送先番号
    let phoneNumber = event.To;
    
    // 転送元番号（Twilio番号)
    let callerNumber = event.Twilio;
    
    // TwiMLインスタンスの作成
    let twiml = new Twilio.twiml.VoiceResponse();

    let dialParams = {};
    let numberParams = {}
    
    dialParams.callerId = callerNumber
    
    // TwiMl生成
    const dial = twiml.dial(dialParams);
    dial.number(numberParams, phoneNumber);
    
    // TwiML返却
    callback(null, twiml);
};

```

作成したFunctionsのPATHをTwiML AppsのRequest URLにペーストします。
これでユーザAからTwilio(TwiML Apps)に架電したときに、Functionsをフックするようになります。
![スクリーンショット 2022-02-26 12.05.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/4bb2238d-d7be-a853-3831-6822e18dbf90.png)

## パターン2(TwiML Bins)
こちらのパターンでは、クライアントからのアクセストークン取得にはTwilio Functions、TwiMLの取得にはTwiML Binsを利用します。

### 構成図
TwiML Binsが追加されます。
![スクリーンショット 2022-02-26 12.12.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/b4a3df89-19e4-e9a7-39ae-7047f84ee29b.png)

### 手順
Functionsのときと同様に、まずはTwiML Appsを作成します。

#### TwiML Binsの作成
`TwiML Bins > My TwiML bins`からTwiML Binsを新しく作成します。
![スクリーンショット 2022-02-26 12.17.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/43a41f13-f703-b67d-2ef7-086a59812655.png)
スクリプトに転送元(Twilio)と転送先の番号を埋め込みます(日本の番号であれば+81が必要)。

```
<?xml version="1.0" encoding="UTF-8"?>
<Response>
  <Dial callerId="+81[Twilio番号]">
    <Number>+81[転送先番号]</Number>
  </Dial>
</Response>
```

最後に、作成したTwiML BinsのURLをTwiML AppsのRequest URLにペーストします。
これでユーザAからTwilio(TwiML Apps)に架電したときに、TwiML Binsをフックするようになります。

# クライアント側の実装について(おまけ)
iOSやAndroidについては公式SDKがあるので、Swift/Kotinを使う場合はドキュメントを読めば普通に実装できる思います。
自分の場合はReact Native(Bare workflow)での実装だったので、以下のライブラリを使いました。

https://github.com/MrHertal/react-native-twilio-phone

スター数もそんなにないので嫌な予感はしていましたが、導入にあたっていくつか罠がありました。
React Nativeで実装する人などほぼいないと思いますが、一応メモとして残しておきます。

## 導入手順の抜け
ドキュメント通りに導入しても、Androidで発信する際にクラッシュしてしまいます。
`proguard-rules.pro`に以下を追記しましょう。

```pro:proguard-rules.pro
##### Twilio Programmable Voice #####
-keep class com.twilio.** { *; }
-keep class tvo.webrtc.** { *; }
-dontwarn tvo.webrtc.**
-keep class com.twilio.voice.** { *; }
-keepattributes InnerClasses
```

## 権限許可モーダルの制御
通話発信を行うためには、iOSであれば「マイク」、Androidであれば「発信と管理」「録音」の権限許可を行う必要があります。
`RNTwilioPhone.initialize(CallKeepOptions, fetchAccessTokenCallback)`の処理タイミングで権限許可モーダルが表示されるので、発信画面の表示タイミングと被らないようにうまく制御しましょう。

## 発信画面の切替(Android)
発信処理を行う以下の処理`RNTwilioPhone.startCall(To, "", Caller, ConnectParams)`で、`To`に番号をいれると、Androidの場合は通話アプリが立ち上がります。
`To`に番号をいれないと自分で作成した発信画面を表示することができるのですが、普通に通話できるのに、「発信できません」というモーダルが表示されてしまいます。

# 参考資料

https://cloudapi.kddi-web.com/availability

https://cloudapi.kddi-web.com/magazine/twilio-lesson/make-ivr-with-twilio-programmable-voice

https://blog.capilano-fw.com/?p=5089#Functions

https://qiita.com/hisayuki/items/42a83e11fab52bfaf0d2

https://qiita.com/official-campaigns/engineer-festa/2022