#他の記事
前置き編(1)
http://qiita.com/chabose/items/2dc18db77bdf14c2738f
発信処理(3)
http://qiita.com/chabose/items/71ea48b0e3bb4fa473fe
作りたいもの
こんなものを作りたかったのですが、発信者番号や、留守電応答や料金の観点からSIPを利用して作っていきたいと思います。
#Twilio側設定
SIPドメインの設定
今回はSIPを利用するので、TwilioのWEBコンソール上からSIPの設定を行います。Twilioにログイン後、「プログラマブルVoice」を選択し、「SIP ドメイン」を選択すると、下記のような画面が表示されるので、
「」を押します。
すると、新規登録画面になるので、
- フレンドリーネーム
- 適当にわかり易い名前
- SIPURI
- 任意の英数字
- Request URL
- そのままでよい
- SIPレジストレーション
- 有効
とし、Voice Authenticationのクレデンシャルリストでを再度クリックします。
すると、ユーザ名とパスワードを入力するところが出てくるので、任意のユーザ名とパスワードを入力します。このユーザ名が、SIPのユーザ名、つまりSIPの番号になります。パスワードは12文字以上、大文字小文字数字混在を求められますので要注意です。
その後、最下部にあるクレデンシャルリストでも、「クレデンシャルリストをクリックして選択します」を押して、先ほど作成したクレデンシャルを選択します。
「保存」をすればSIPドメインの設定は完了です。
登録されたSIPの情報
- ユーザ名/パスワード
- クレデンシャルに登録したユーザ名・パスワード
- SIPのドメイン
- {入力したSIP URI}.sip.us1.twilio.com
となります。
ソフトフォンに登録してみる
上記のアカウント情報を使って、実際にSIPサーバーに登録してみます。
今回はアプリ上で動作するソフトフォンを利用します。
設定はソフトフォンにかなり依存してくるのでご利用のアプリケーション、もしくはIP電話などに合わせて設定する必要があるのですが、今回はAcrobits Softphoneという有料のiPhoneアプリを利用しています。
選定理由は
- CallKit対応しているから
- Push通知による代理待受が可能
というだけです。CallKitはiPhoneにおいて、通常の電話と同じUIで着信を表示したり、発信したりすることが出来る iOS10からの新機能です。LINEやSkypeなどにはすでに導入されているようです。これがあるとほぼ通常の電話と区別がつきません。
Push通知についてはCallkitと関連するのですが、これを利用するとアプリ側のプロキシサーバー(SIPIS)に代理でsip registrationをし、着信があるとクライアント側にApple経由でPush通知がとび、端末が着信します。
着信を取ると、通話自体はダイレクトに接続され、SIPはアプリ側のサーバー経由でリレーされてきます。これですと、アプリが常時SIPサーバーと通信している必要もないですし、着信がNATを越えられずに届かないこともありません。1
ユーザー名とパスワードがおそらく代理サーバーにそのまま蓄積されますので、セキュリティ的な不安がある場合は利用は避けたほうがいいかもしれません。(Acrobits自体はT-mobileにもアプリ提供をしてますし、大きな問題はないかと思いますが)
なお、プロキシサーバー(SIPIS)についてはこちらから自分で立てることもできるようです。https://doc.acrobits.net/sipis/installation.html
Acrobits Softphoneの設定例はこんな感じです。
「着信」を「プッシュ通知」にすることで上記の機能が有効になります。
callkit未対応でよければZoiperなどが良いのではないでしょうか。
ようやく実装
実装すべき項目
- 着信電話のSIPへの複数転送
- 発信者番号の表示・1番先に取った者と通話(twilioの機能でできる)
- 営業時間外のアナウンス
- Slackへの着信通知
- Twilioの番号からの発信
着信電話のSIPへの複数転送
Twilioは、基本的に(APIを使わなければ)番号に対する着信に対して、指定したURLを呼び出し、そこから帰ってきたXML(TwiML)をもとに次の制御を決めていく仕組みになっています。
この場合に返すべきXML(TwiML)を書くと、下記のようになります。
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Dial callerId="かけてきた人の番号">
<Sip>ユーザ名@SIPドメイン名.sip.us1.twilio.com</Sip>
</Dial>
</Response>
<Response>
は、TwiMLのルート要素で、どのTwiMLにも必ず入る必須の要素です。
<Dial>
で、着信した電話を転送することを示しています。callerID
は転送先に表示される発信者番号です。SIPに転送する場合は任意の番号を入れることができます。
<Sip>
は、SIPを利用し、要素内に書かれたアドレスを発信先にすることを示しています。今回は、先程設定したSIPドメインと、追加したクレデンシャルのユーザ名を組み合わせたものを使います。
複数人につなぐはずなのに宛先が一つなのは、一つのSIPアカウントに複数人でレジスト(登録)することが出来るためです。(1人1アカウントにしてもいいと思いますが)
SIPが繋がらなかったらどうしよう
これだけでもしアプリの問題や、不具合で繋がらなかったりすると辛いので、
もしSIPで繋がらなかった場合は携帯電話に転送するようにします。
バックアップ系なので、留守番電話に転送されたらそれはしょうがないという割り切りで。
SIPで繋がらなかった場合、相手もしばらく発信音を聞いていて、しびれを切らしている可能性があるので、少しアナウンスを流してもう少し待ってもらうようにお願いします。
下記の様なXMLに変更します。
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Dial callerId="{かけてきた人の番号}" timeout="10" action="{Dialが完了したときに呼ばれるURL}">
<Sip>ユーザ名@SIPドメイン名.sip.us1.twilio.com</Sip>
</Dial>
</Response>
<Dial>
に、timeout="10"
というプロパティが追加されています。これでの動作のタイムアウトを設定します。加えて、action=""にURLを設定すると、<Dial>
が完了した(終話、タイムアウト、切れたなど)場合に、指定したURLにPOSTしTwiMLを読み込みにいきます。
actionを利用して、通話がタイムアウトになった場合、下記をTwiMLを返答して携帯電話に転送するようにします。
通話がタイムアウトになった場合、下記のTwiMLを返すようにします。
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say language="ja-jp">お繋ぎしています。しばらくお待ちください。</Say>
<Dial callerId="Twilioで購入した番号">
<Number>+8190XXXXXXXX</Number>
<Number>+8180XXXXXXXX</Number>
</Dial>
</Response>
<Say>
は機械音声によるアナウンスです。language="ja-jp"
を指定することで、日本語でのアナウンスが可能です。
次の行では、通常の電話への転送を行っています。この場合はcallerID
に指定できるのはTwilioで購入した番号のみとなります。なお、基本的にTwilioで使う電話番号の形式はE.164形式と呼ばれる、「+国番号と先頭0を抜いた電話番号」(ex. +81312345678)で記載する必要があります。
<Number>
タグのコンテンツにE.164形式で発信したい番号を記載しています。
複数<Number>
タグを書くことで複数の同時発信と、最初に受話した人との接続、を行うことができます(最大10件)
営業時間外のアナウンス
前の記事ですでに書きましたが、ここは単純に土日、かつ営業時間外の場合にTwiMLを出し分けるコードを書くだけです。祝日を取得...は今回はパスしました。
例では土日と18時以降、9時前の場合アナウンスを流します。
つくってみる
後ほどLambda+Node.jsにしますが、単純な動作確認なのでPHPで書いてみます。
今回はファイルが2つ、incoming.phpとtransferResult.phpの2つを用意しました。
incoming.phpはTwilioに着信があった際に呼ばれるもの、
transferResult.phpは転送通話が完了した際に呼ばれるもので、受話できなかった場合に通常の携帯電話に転送する役割を担います。
こんなイメージ。
<?
header('Content-Type: application/xml');
//固定値の設定。接続先SIP、購入した050番号、転送先番号
$sipAddr = "fuga@hogehoge.sip.us1.twilio.com";
$myNumber + "+8150XXXXXXXX";
//TwilioのPOSTから発信元番号を取得
$callerid=htmlspecialchars($_POST['From']);
//タイムゾーンを設定
date_default_timezone_set('Asia/Tokyo');
//時刻と曜日(0-6)を取得
$currentHour = (int)date('H');
$currentDay = date('w');
$resp = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>";
//土日-営業時間外判定
if($currentHour >= 18 || $currentHour < 9 || $currentDay == 0 || $currentDay == 6 ){
$resp .= '<Say language="ja-jp">今日の出前受付は終了しました。</Say>';
}else{
$resp .= <<<EOM
<Dial timeout="10" callerId="${callerid}" action="transferResult.php">
<Sip>${sipAddr}</Sip>
</Dial>
EOM;
}
$resp .= "</Response>";
echo($resp);
?>
内容は大層なものではなく、先程作ったTwiMLを時間ごとに出し分けること、POSTから受け取った発信者番号をTwiMLに貼り付けている、のみです。
<?
header('Content-Type: application/xml');
$outGoingNumber = array("+8190XXXXXXXX","+8190XXXXXXXX");
$myNumber = "+8150XXXXXXXX";
//転送先番号のXML化
$numberFormatted="";
for($i=0;$i<count($outGoingNumber);$i++){
$numberFormatted .= "<Number>{$outGoingNumber[$i]}</Number>";
}
$resp = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>";
//DialCallStatus="no-answer/busy/failed"の場合
switch($_POST['DialCallStatus']){
case "no-answer":
case "busy":
case "failed":
$resp .= <<<EOM
<Say language="ja-jp">別の電話にお繋ぎしています。</Say>
<Dial callerId="${myNumber}">
${numberFormatted}
</Dial>
EOM;
break;
default:
break;
}
$resp .= "</Response>";
echo($resp);
?>
こちらはPOSTのDialCallStatusの値が"no-answer/failed/busy"、つまり10秒以内にSIPが着信しなかった、話中、もしくは着信に失敗した場合に通常の電話に転送するようにしています。
Slackへの通知
今回は、
- Twilioで取得した050番号に着信したとき
- 転送先の通話が開始されたとき
- 転送先の通話が終了したとき
にSlackに通知するようにします。
Twilioには通話のステータスを任意のURLに通知する機能があります。
購入した番号への着信や終話を検知するには、Twilioのコンソールから設定します。
メニューから、電話番号->設定したい番号を選択->CALL STATUS CHANGESに
着信があった場合に呼ばれるURLを指定できます。
一方、転送した電話などTwiML起因で発生した通話については下記のようにTwiMLで指定します。
<Number statusCallback="{URL}" statusCallbackEvent="{情報を得たいステータス}">
<Sip statusCallback="{URL}" statusCallbackEvent="{情報を得たいステータス}">
{情報を得たいステータス}に入る値は下記の通り。
イベント | 説明 |
---|---|
initiated | Twilioがダイヤリングを始めた時、initiatedイベントが発動します。 |
ringing | 電話が鳴り始めると、ringingイベントが発動します。 |
answered | 通話を開始(受話)するとansweredイベントが発動します。 |
completed | completedイベントは端末の状態に関係なく(busy、canceled、completed、failed、no-answer)コールが終了した時に発動します。 StatusCallbackEventが指定されない場合、completedがデフォルトで発動されます。 |
(Reference: https://jp.twilio.com/docs/api/twiml/sip#attributes-status-callback-event) |
ここから、取得したいステータスをstatusCallbackEventにスペース区切りで羅列します。
今回は、転送先の電話が応答したとき、終話した時にSlackに通知したいので、
<Number statusCallback="{URL}" statusCallbackEvent="answered completed">
<Sip statusCallback="{URL}" statusCallbackEvent="answered completed">
のように先程のTwiMLを変更します。
Slack側の設定
Slack側はIncoming Webhookの設定を事前に行っておきます。
https://slack.com/signin?redir=%2Fservices%2Fnew%2Fincoming-webhook
このあたりは
Qiita:SlackのWebhook URL取得手順
が等に詳細があるため、説明は省略します。
実装
電話番号に着信があった場合の通知
<?
//Slack Incoming Webhooks URL
$url = 'https://hooks.slack.com/services/XXXXX/XXXXXXXX';
header('Content-Type: application/xml');
//固定値の設定。接続先SIP、購入した050番号、転送先番号
$sipAddr = "fuga@hogehoge.sip.us1.twilio.com";
$myNumber + "+8150XXXXXXXX";
//TwilioのPOSTから発信元番号を取得
$callerid=htmlspecialchars($_POST['From']);
//タイムゾーンを設定
date_default_timezone_set('Asia/Tokyo');
//時刻と曜日(0-6)を取得
$currentHour = (int)date('H');
$currentDay = date('w');
$resp = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Response>";
//土日-営業時間外判定
if($currentHour >= 18 || $currentHour < 9 || $currentDay == 0 || $currentDay == 6 ){
$resp .= '<Say language="ja-jp">今日の出前受付は終了しました。</Say>';
}else{
$resp .= <<<EOM
<Dial timeout="10" callerId="${callerid}" action="transferResult.php">
<Sip statusCallback="statusUpdate.php" statusCallbackEvent="answered completed">${sipAddr}</Sip>
</Dial>
EOM;
}
$resp .= "</Response>";
echo($resp);
//Slack通知の実装
$data = array(
'text'=> $callerid."から自動応答に着信がありました",
'username' => "でんわばん"
);
//HTTPリクエストの作成
$options = array(
'http' => array(
'method' => 'POST',
'content' => json_encode($data),
'header'=> "Content-Type: application/json\r\n" .
"Accept: application/json\r\n"
)
);
$context = stream_context_create($options);
//リクエストの発行
$result = file_get_contents($url, false,$context);
?>
前回との差分は、<Sip>
タグに<Sip statusCallback="statusUpdate.php" statusCallbackEvent="answered completed">
を追加して転送電話のステータスをstatusUpdate.phpに転送できるようにしたことと、
下部のSlack通知の実装部分で、textをキー、送るテキストをvalueとしたメッセージを送信しています。usernameはSlack側に表示されるユーザー名となります。特に変なことはやっておらず、配列をJsonに変換してHTTP POSTに載せてSlackのwebhookエンドポイントに飛ばしているだけです。
転送先の通話に変化があったときの通知(転送電話をうけた・終話した)
<?
//Slack Incoming Webhooks URL
$url = 'https://hooks.slack.com/services/XXXXX/XXXXXXXX';
$message='';
//Callstatusに乗ってくるパラメータによって応答を分ける
switch($_POST['CallStatus']){
case 'in-progress':
$message = $_POST['To'] . 'が応答したようです。';
break;
case 'completed':
$message = $_POST['From'] . 'との通話が終了しました。';
break;
default:
break;
}
$data = array(
'text' => $message,
'user' => 'でんわばん'
);
//HTTPリクエスト作成
$options = array(
'http' => array(
'method' => 'POST',
'content' => json_encode($data),
'header'=> "Content-Type: application/json\r\n" .
"Accept: application/json\r\n"
)
);
$context = stream_context_create($options);
//HTTPリクエスト送信
$result = file_get_contents($url, false,$context);
?>
こちらも上記とほとんど同様で、Slackへの通知を行っています。このファイルは上部のincoming.phpのstatusCallback
に指定されているファイルなので、通話の状況がPOSTでデータが飛んで来ます。その中から、CallStatus
をSwitch文で分岐し、通話が始まったときと終了したときにメッセージを送信しています。
ここまで
- 発信については力尽きたのでここまで。
- 次に発信の実装とLambda/API Gatewayへの移植を行います。
-
NATは予想ですが... ↩