技術は私たちの力。技術は私たちの楽しみ。 Creative Developer BLOG 技術部ブログ
Technology is our strength. Technology is what we enjoy.

SUBLINEに保留転送機能をつけてみた

自社の電話アプリサービスSUBLINEには様々な機能が実装されていますが、実は保留機能は実装されていません。社外からの電話を一時的に保留にし、特定の担当者にオペレーターが電話を転送する機能があれば便利だと思ったことはありませんか?今回は長年実現が困難だと考えられていた保留転送機能に挑戦してみました。

はじめに

今回の機能に挑戦するにあたり、通話中のアプリの画面改修やリクエストは斎藤さんにご協力いただきました。お忙しい中誠にありがとうございました。
(また、大原さんやTwilioサポートの方にも質問させていただくことございました。快くご回答くださりありがとうございました。)

保留転送機能の概要

今回実現を目指した保留転送機能の動作概要です。下記の動作を実現できることをゴールとしました。

1:オペレーターが保留ボタンを押すことで音楽が再生される。
2:保留終了ボタンを押すことで通話を(何度でも)再開できる。
3.保留中に転送先番号を入力してその番号へ転送することができる。
4.転送先とオペレーター(転送者)が一度通話できる。
(会話例:〇〇商事の□□さんからのお電話です、お繋ぎしてよろしいでしょうか?)
5:「発信元と繋げる」ボタンでオペレーターの通話は終了。発信元と転送先の会話がスタートする

2つの実現方法

今回はTwilioというAPIプラットフォームサービス、特にTwilio Programmable Voiceが提供する機能を使って実現していきたいと思います。

Programmable Voiceを語る上で欠かせないのが、電話やSMSサービスを制御するためのXMLベースのマークアップ言語、TwiMLです。

TwiMLでは動詞は行うアクションを、名詞はリソースを指します。Dialは発信用の動詞、Numberは特定の番号を示す名詞、といったイメージです。今回触れるConferenceやEnqueueもこの一種になります。

Twilioの保留と言えば真っ先に挙げられるのがConferenceですが、今回はEnqueueを使って保留転送機能を実現しました。まず、この二つの機能について軽く触れたいと思います。

Enqueueについて

今回使用した機能になります。名前の通り通話を「列」に並ばせておくようなイメージです。

コールセンターなどで、たくさんの順番待ちが発生している際にいったん列に通話を並ばせておいて、先に入ったものからオペレーターに繋いでいくというケースでよく使われるようです。本格的なコールセンターではTask Routerなどと組み合わせて適切な業務の振り分けを行うのに欠かせない機能となりそうですね。

今回はこの「列に並んで待ってもらう」部分を保留に利用させてもらいました。

Enqueueの使い方をざっくり説明するとEnqueueで特定の列名をつけつつ現在の通話をその列に並ばせておき、Dialで特定のQueue名を指定した際に、先に入った通話から一つづつ繋げるイメージです。

Conferenceについて

その名の通り会議をイメージした通話方法で、多人数の電話会議を実現することができます。Enqueueよりもできることが多いのでいくつか例を示します。

・入退室時にビープ音がなる
・特定の参加者が入るまで会議が開催されないようにできる
・特定の参加者が退出すると会議が終了するようにできる
・特定の参加者のミュート状態(聞くだけで発言できない)ON/OFFを切り替えできる
・会議の録音ができる

色々と機能がありますね。もちろん保留・転送も可能です。保留・通話再開くらいはできるか挙動確認しました。確認した時はほとんどEnqueueと同じ方法で保留でき、挙動の違いは入退室時のビープ音があるかどうかくらいだったので今回は詳細については割愛します。

[ちょっと雑談]では何故Enqueueだったの?

EnqueueとConferenceを比較してみましたが、どちらも同じことができるなら多機能な方を選ぶべきだと思った方も多いのではないでしょうか?そこで本筋とは少し逸れますが、Enqueueを使用した背景に触れようと思います。

Enqueueを選んだ理由を雑に列挙すると下記の通りです。
・単純に2つ調査する時間が無かった
・Conferenceには今回の実装に関係の無い機能が多かった。
・通常の通話イメージとの乖離が大きい。(列に並んで少し待ってもらう程度なら大きな抵抗はないけれど、皆で同じ会議室に向かって発信するイメージだと少し抵抗がある。)

もちろんConference自体はできることが多く、十分に魅力的な選択肢の一つです。今回は時間の都合上調査もままなりませんでしたが、もし製品化する場合はEnqueueと併せて再調査した上でどちらを使うのか決めることになるでしょう。

保留転送の実現までの流れ

下記の順番にみていこうと思います。
・親通話、子通話の概念を把握。
・APIで通話を更新する方法を把握。
・保留の開始・通話の再開。
・オペレーターと転送先を繋げる。
・発信元と転送先を繋げる。

親通話、子通話の概念を把握

どのように保留・転送を行なっているか説明する前に、Twilioの発着信を理解する上で重要な親通話・子通話という概念について軽く触れておきます。

外部の番号からTwilioの番号に着信があった場合、まずTwilioサーバーと繋がります。(親コール)

Twilioサーバーではその番号の設定を参照してその後の振る舞いを決定する。(TwiMLの返却)

今回の例ではTwilioサーバーから特定の番号へ発信。(子コール)

Twilioサーバーを介して外部番号とTwilio番号間で通話が成立する。

つまりTwilioでは1本の通話のように見えても、「かかってきたコール」と「かかってきたコールで実行された、TwiMLがかけた新しいコール」は親通話、子通話という別々の物として区別されています。

APIで通話を更新する方法の把握

Twilioではアクティブな通話を一つのResourceとみなし、Updateをかけることで様々な操作が行えるAPIがあります。セキュリティの観点からあまりサブラインのコードを外部に公開したくはないので、今回はTwilioが公開しているこれらのAPIを中心に話を進めていきたいと思います。下記のURLから公式のドキュメントを確認できますので、ご興味ある方は読んでみてください。

https://www.twilio.com/docs/voice/api/call-resource#update-a-call-resource

基本的には上記のように通話毎に割り振られたsidに向けて更新のリクエストを出すことになります。Twilioコンソールを触る人はよく見るCallSid(DialCallSid, ParentCallSid)というパラメタと同じ物ですね。

APIで通話を更新する際のイメージ

内部的なコードを公開することは抵抗があるので実際のコードは載せていませんが、下記のようにアクティブなCall Resourceに向けてAPIを叩くことで通話をUPDATEしていっています。


curl -X POST "https://api.twilio.com/2010-04-01/Accounts/$TWILIO_ACCOUNT_SID/Calls/CAXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX.json" \
--data-urlencode "Twiml=(Response)(Say)Ahoy there(Say)(/Response)" \
-u $TWILIO_ACCOUNT_SID:$TWILIO_AUTH_TOKEN

上記は特定の通話をメッセージ読み上げに更新するようなリクエストですね。(エスケープの関係で一部実際のTwiMLと異なる箇所があります。TwiMLの仕様については公式サイトをご覧ください。)

保留の開始

保留開始のリクエストを受けっとたらサーバー側でその通話のUPDATEを行いその通話を特定の部屋へEnqueueします。(列と言った方が良いかもしれませんが、今後部屋という表現をさせていただきます。)

ConferenceやEnqueueで待機部屋名が重複してしまうと意図せぬ通話が成立してしまうので、部屋名は重複しないように作成しましょう。製品化するのであれば顧客id,unixtime,randあたりも組み合わせながら一意のidを部屋名としておいた方が安全だと思います。

どのように実現しているのか詳細は伏せますが、今後の流れにおいて大事なのは後でどの通話が元になったのかわかる一意の部屋名に親通話と子通話をそれぞれ待機させるということです。

他に注意する点としては、今回通話をUPDATEするのは子通話の方だということです。親通話の方が更新されてしまうと、元となる通話を失った子通話の方は即切断されてしまいます。また、親通話の側も子通話の切断を感知して自身をキューに入れなければなりません。

※多くのTwiMLの動詞・名詞ではそれが終了した際に叩くURLや特定のイベントが起こった際(電話が鳴り始めた、通話が開始した、切断された)に叩くURLを指定でき、任意のパラメタをつけることもできます。
この時に今どんな状況か把握できるようにリクエストを作成することが、状況に合わせて通話を自由に操作する際に肝要になってきます。

今回は特定の状況下にあることを検知するために着信設定の一つとしてオペレーターモードを用意しておきました。

※保留転送は必ずしもオペレーターが操作するものではなかったり、特定のキューの名前を部屋名と呼ぶとConferenceっぽく聞こえてしまうことあるかもしれませんが、説明のし易さを考慮して便宜上そのように呼ばせていただいております。

通話の再開

通話を再開する時は子通話を更新し、Dialの対象に親通話が待機しているキュー名を指定することで、再度通話が繋がります。

再(保留・通話再開)の場合も基本的に同じです。
子通話は保留開始で子供部屋へ移動し、通話再開で再度親部屋に繋げる、を繰り返すことになります。親通話の場合は保留により通話が切れたことを感知して自身をキューに待機させる、という動作を繰り返します。

オペレーターと転送先を繋げる

ここまで来れば何となくわかると思いますが、転送先に繋げる場合も基本的な流れは同じです。サーバーにきた転送のリクエストに基づいて、通話を更新して別の宛先への発信を指示します。

ちなみに流れの途中で誰かが電話を切断するなどした場合、パターンに応じて任意の通話を切断できるようにしておかないと永遠に待機部屋で待たされることになるため、製品化の際はどのような挙動を正とするかを含めて切断処理は丁寧に作る必要がありそうです。

親通話は待機状態で放置したまま、子通話を先に新しい転送先に繋げることになります。
今回は一度特定のエンドポイントを叩かせてresponseで再度Dial発信をしています。
この発信はこれまで成立していたコールとは全く異なる発信であるため、当然別のsidになります。
(子通話のsidを更新して生まれた発信なので、イメージ的には孫通話みたいなものと捉えてもいいかもしれません。)

この通話を後で発信元と繋げたいので、最初の親通話と最新の通話を何かしらの方法で結びつける手段が必要です。今回はシンプルにDBに記録しておきました。
ここまでくればあと一息ですね。

発信元と転送先を繋げる

それでは今の状況をおさらいしてみましょう。
・発信者の通話はキューに入って待機状態です。Queueの名前や通話のsidは特定できる状況です。
・オペレーターと転送先は通話中です。これは最新の通話のため、上記の発信者の通話とは直接的な関係がありませんが、DB等を介して繋がりを辿ることができます。

あとはご想像通り、最新の通話のsidを指定して最初の通話のQueueに対して電話を掛けさせれば今度は発信元と転送先の通話が成立しますね。これで目標とした動作達成完了です。ありがとうございました。

最後に

個人的には十分な調査や作り込みができない部分も多く悔しさが残ったのですが、今まで要望はあったが実現はされていなかった挙動を実際に確認できたことに一定の意義はあったかと思います。今後はもっと早く準備に取り掛かれるようにしたいですね。

記事一覧へ