【Amazon Connect】「電話していい?」を自動化する(2人だけのホットライン)

タクミ「もしもし、いま電話できる?」
ミズキ「ごめん、料理中だから忙しい」
タクミ「わかった、またあとで電話する」
ミズキ「あ、終わったらこっちから電話するよ」
タクミ「わかった。待ってる」

どうも、電話が苦手なのに Amazon Connect をやっている寺内です。
冒頭から、小芝居ですみません。

携帯電話が普及し、いつでも誰でも直接電話でつながるようになり、こんなやり取りを何度もしていると思います。 電話は同期的なコミュニケーションです。電話を掛ける方と受ける方が同時に時間を専有します。
最初、電話を掛ける方は相手の状況を知らないまま電話を掛けます。「いま電話したら迷惑かな」という心配をしながら。

受ける方は、今やっていることを中断して電話に出ざるをえません。そして手が離せないことを伝える必要があります。
その対処として、より手軽な非同期コミュニケーション手段であるLINEなどのインスタントメッセージで、相手の都合や状態を確認する事前調整をすることになります。「今から電話していい?」「いいよ」「じゃあ電話するね」という感じです。

これって面倒くさくないですか。

そこで、この同期コミュニケーションを始める前の手続きを、Amazon Connectを使って簡略化してみましょう。電話での通話開始のためのネゴシエーションプロトコルを作ります。

利用方法

ここで作るホットラインシステムは、特定の二人の専用のシステムとします。
ホットラインシステムには予め二人の電話番号と名前を登録しておきます。
このホットラインシステムを使いタクミくんがミズキさんに電話したい場合を例に取り手順を説明します。

はじまりの手順

  1. まずタクミくんがホットラインシステムに電話をする。その呼はすぐ切れる。これでホットラインシステムは、タクミくんがミズキさんに電話を掛けたがっていることを認識する。
  2. ホットラインシステムはミズキさんにSMSを送る。メッセージ内容は以下のとおり。
    タクミさんが電話で話したがっています。話せる場合は、以下リンクをタップしてください。 http://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/accept?contactid=xxxxx

    このメッセージを見たミズキさんは今の状況から通話するかどうかを判断できます。

ミズキさんが通話できる状態の場合

  1. ミズキさんは、SMS本文にあるURLリンクをタップして、ホットラインシステムに通話する意思を示します。
  2. ホットラインシステムは、ミズキさんが通話可能である旨を受け取り、タクミくんにSMSで以下のようなメッセージを送り、ミズキさんの意向を伝えます。
    ミズキさんは通話できます。xxx-xxx-xxx に電話してください。
  3. タクミくんは、そのSMSメッセージの電話番号リンクをクリックし、ミズキさんとの通話を始めます。

ミズキさんが通話できない状態の場合

  1. ミズキさんは何もしません。
  2. タクミくんは、2分後にホットラインシステムから以下のSMSを受け取ります。
    ミズキさんは今は通話できないようです。折り返し電話が来るのを待ちましょう。
  3. その後、ミズキさんが電話をできる状態になったら、ホットラインシステムに電話をし、タクミくんが話せる状態かどうかの確認を開始します。(1番からタクミくんとミズキさんが入れ替わった手順が始まる)

アーキテクチャ

システム構成は以下のとおりです。利用方法の順に従い動作を説明します。

はじまりの動作

  1. アーキテクチャ図の左上のタクミくんが、まず最初にAmazon Connect に電話をします。
    Connectのコンタクトフローでは、A.Lambdaを起動します。このLambdaで、S3から設定ファイルを読み込み、タクミくんとミズキさんの名前と電話番号を得ます。そして、A.Lambdaは今電話してきた発信者電話番号と照合し、タクミくんとミズキさんのどちらが電話をしてきたを認識し contact_direction 構造体のもととなるデータを作成し、Connectコンタクトフローに返します。
    どちらの電話番号でもない場合は、電話を切断して終了します。
  2. 続けてConnectは、B.Lambdaを起動します。このLambdaでは、上で受け取った誰から誰への通話かの方向データを基にcontact_direction構造体を作ります。
    そしてコンタクトIDをキーとしステータスをinqueryにして、contact_direction 構造体と共にDynamoDBに保管します。
    次に、Amazon SQSにメッセージを作成し送信します。このキューには、2分間のち遅延配送が設定されており、2分間のタイマーが始まります。
    最後に、ミズキさん宛にAmazon SNSでSMSメッセージにAPI Gatewayのリンクを含めて送信します。

ここで、ミズキさんは通話できる状況であるかどうかを判断します。

通話できる場合の動作

  1. ミズキさんは、SMSメッセージにあるリンクをタップし、API Gatewayへアクセスします。
    API GatewayはリンクにあるコンタクトIDのパラメータをD.Lambdaに渡します。
    API Gatewayへのアクセスがあったということは、ミズキさんは通話できる状況であるという意思表示です。
  2. API Gatewayから起動されたD.Lambdaは、ミズキさんへ電話をしてよい旨のSMSメッセージをタクミくんへ送ります。 同時に、DynamoDBの当該コンタクトIDのステータスにacceptを書き込みます。この書き込みにより、2分後に動作するC.Lambdaがステータスを確認し、ミズキさんが通話をできる状況になったと判断します。
  3. タクミくんはミズキさんの電話番号を埋め込んだSMSメッセージを受信するので、その電話番号をタップすることでミズキさんに電話が通じます。

ミズキさんが通話できない状況であった場合の動作

  1. ミズキさんは忙しくて手が離せいない状況(もしくはスマートフォンから離れている状況)なので、なにもする必要はありません。
  2. B.LambdaによってAmazon SQSに送信されたメッセージは、2分間の遅延配信がされます。
    その2分間にミズキさんがリンクをタップして応答してくれることを期待していましたが、残念ながら応答はありませんでした。
    2分間タイマーがタイムアウトすることでC.Lambdaが起動します。 C.LambdaはDynamoDBから当該コンタクトIDを検索し、ステータスを確認します。ステータスはinqueryのままなので、ミズキさんが応答しなかったことがわかります。
    そこで、Amazon SNSでタクミくんに、ミズキさんは通話できない状況である旨のSMSメッセージを送信します。
  3. タクミくんはしばらく待つしかありません。
    ミズキさんは、届いていたSMSメッセージに気づくと、改めてホットラインシステムに電話をかけて、タクミくんが通話できる状況であるかどうかを確認するプロセスが再び始まります。

以上が、今回のホットラインシステムの動作になります。

システムの作成

では、このホットラインシステムを作成していきましょう。 作成順は以下のようになります。

  1. Lambda用IAMロール作成
  2. 設定ファイル作成とS3へのアップロード
  3. Amazon SQSのキュー作成
  4. Amazon SNSの設定確認
  5. DynamoDBテーブル作成
  6. Lambda関数を4つ作成
  7. Amazon Connectインスタンス作成
  8. API Gateway作成

では順番に作っていきます。

Lambda用IAMロール作成

Lambdaでは以下のサービスを使用します。本当は各Lambdaごとに適切なサービス権限を作ったほうがいいですが、ここでは手抜きして全Lambda共通のIAMロールを作成します。許可する権限は以下の通りです。

  • AmazonSQSFullAccess
  • CloudWatchFullAccess
  • AmazonDynamoDBFullAccess
  • AmazonS3ReadOnlyAccess
  • AmazonSNSFullAccess

設定ファイル作成とS3へのアップロード

設定ファイルは以下の通り、JSONの配列です。 ファイル名は phonenumber.json で決め打ちです。

[
     {"name" : "タクミ", "phone" : "+8190AAAABBBB"},
     {"name" : "ミズキ", "phone" : "+8170XXXXYYYY"} 
]

ファイルができたら、任意のS3バケットにアップロードします。S3バケット名は、Lambda関数の環境変数で指定します。

Amazon SQSのキュー作成

SQSのキューを新規で1つ作成します。キュー名は任意の名前としてください。キュー名は、Lambda関数の環境変数で指定します。
配送遅延を2分に指定します。この時間が、発信者が受信者の応答を待つ時間となります。

Amazon SNSの設定確認

SNSトピックスを作る必要はなく、Amazon SNSのモバイルテキストメッセージング (SMS)を使用します。
もしサンドボックスのままでしたら、サンドボックスの送信先電話番号に検証済電話番号として、二人の電話番号を登録しておいてください。
また、サンドボックスでは月額送信料が制限されています。制限を超えるとSMS送信ができなくなりますので注意してください。必要なら、上限緩和申請をしてください。

DynamoDBテーブル作成

DynamoDBのテーブルを1つ作成します。テーブル名は任意の名前としてください。テーブル名は、Lambda関数の環境変数で指定します。
パーティションキーはcontact_idとし文字列型を指定します。

Lambdaを4つ作成

Lambda関数を4つ作成します。全てランタイムはPython3.9です。関数名は任意でかまいませんが、呼び出し元であるAmazon ConnectやAPI Gatewayで適切に設定してください。

 A.Lambda

動作の項で説明した、S3バケットから設定ファイルを読み込み、電話番号の情報を作成します。
環境変数は以下を設定してください。

  • bucket : バケット名(例:hotline-bucket)
import json
import boto3
import os

def lambda_handler(event, context):
    print("JSON type event")
    print(json.dumps(event))

    # 電話番号を取得する

    # 環境変数からバケット名を受け取る
    FILENAME = 'phonenumber.json'
    backet_name = os.environ['bucket']
    client = boto3.resource('s3')
    bucket = client.Bucket(backet_name)

    # バイナリ型で取得したS3上のファイル内容objをJSON形式の文字列型として解釈する
    obj = bucket.Object(FILENAME)
    json_phones = obj.get()['Body'].read()
    # 文字列型JSONを辞書型に変換する
    dict_phones = json.loads(json_phones)

    # 発信者の電話番号をイベント情報から得る
    caller_phone = event['Details']['ContactData']['CustomerEndpoint']['Address']

    # 発信者がリストに含まれるかをチェックする
    # 含まれていなかったら、Connectにエラー(400)を返す
    caller_name = None
    for item in dict_phones:
        if (item['phone'] == caller_phone):
            caller_name = item['name']
    if (caller_name == None):
        print('登録されている電話番号ではありません。')
        return { 'statusCode': 400 }

    # 発信者ではない方の電話番号をdict_phones から特定する
    for item in dict_phones:
        if (item['phone'] != caller_phone):
            reciever_name = item['name']
            reciever_phone = item['phone']

    # 発信者と受信者を特定して返答する
    return {
        'statusCode': 200,
        'caller_phone': caller_phone,
        'caller_name': caller_name,
        'reciever_phone': reciever_phone,
        'reciever_name': reciever_name
    }

B.Lambda

このLambdaでは、DynamoDBへの書き込み、Amazon SQSへの送信、Amazon SNSへの送信を行います。
環境変数は以下を設定してください。

  • apigw_url: 後に作るAPI GatewayのURL(例: https://XXXXXXXX.execute-api.ap-northeast-1.amazonaws.com)
  • dynamo_table: DynamoDBテーブルの名前(例: hotline-contact)
  • sqs_url: SQSキューのURL(例: https://sqs.ap-northeast-1.amazonaws.com/999999999999/hotline-sqs)
import json
import boto3
import os

def lambda_handler(event, context):
    print("JSON type event")
    print(json.dumps(event))

    # Contact IDを得る
    contact_id = event['Details']['ContactData']['ContactId']
    # ホットラインシステムの電話番号を得る
    System_endpoint = event['Details']['ContactData']['SystemEndpoint']
    # Eventパラメータから、発信者と受信者を得る
    caller_name    = event['Details']['ContactData']['Attributes']['caller_name']
    caller_phone   = event['Details']['ContactData']['Attributes']['caller_phone']
    reciever_name  = event['Details']['ContactData']['Attributes']['reciever_name']
    reciever_phone = event['Details']['ContactData']['Attributes']['reciever_phone']

    # コンタクトの方向を辞書型にしておく
    contact_direction = {
        'contact_id':contact_id,
        'System_endpoint':System_endpoint,
        'caller_name':caller_name,
        'caller_phone':caller_phone,
        'reciever_name':reciever_name,
        'reciever_phone':reciever_phone,
    }

    # DynamoDBに登録
    # 環境変数からテーブル名を得る
    table_name = os.environ['dynamo_table']
    
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table(table_name)
    # DynamoDBテーブルに、コンタクトIDとinqueryステータスを書き込み
    try:
        response = table.put_item(
           Item={
                'contact_id': contact_id,
                'status': 'inquery',
                'contact_direction': contact_direction
            }
        )
    except Exception as e:
        print('[Error] DynamoDB へのput_item に失敗しました。')
        print(e)
        return { 'statusCode': 400 }

    # SQS送信
    # 環境変数からSQS URLを得る
    sqs_url = os.environ['sqs_url']

    sqs = boto3.client('sqs')
    try:
        response = sqs.send_message(QueueUrl=sqs_url, MessageBody=json.dumps(contact_direction))
    except Exception as e:
        print('[Error] SQS送信に失敗しました。')
        print(e)
        return { 'statusCode': 400 }

    # SNS送信
    # 環境変数からAPI Gateway URLを得る
    apigw_url = os.environ['apigw_url']
    # メッセージ本文定義
    sns_body = caller_name + 'さんが電話で話したいそうです。通話できるならリンクをタップしてください。 ' + apigw_url + '/accept?contactid=' + contact_id
    print(sns_body)

    sns = boto3.client('sns')
    try:
        response = sns.publish(
            PhoneNumber = reciever_phone,
            Message = sns_body
        )
    except Exception as e:
        print('[Error] SNSでのSMS送信(' + reciever_phone + ')に失敗しました。')
        print(e)
        return { 'statusCode': 400 }

    return { 'statusCode': 200 }

C.Lambda

このLambdaでは、受信者が通話に応答できない場合の処理を行い、発信者にSMSメッセージを送信します。 環境変数は以下を設定してください。

  • dynamo_table: DynamoDBテーブルの名前(例: hotline-contact)
  • sqs_url: SQSキューのURL(例: https://sqs.ap-northeast-1.amazonaws.com/999999999999/hotline-sqs)
import json
import boto3
import os

def lambda_handler(event, context):
    print("JSON type event")
    print(json.dumps(event))

    # Event情報から、コンタクト情報を取り出し辞書型に読み込む    
    contact_direction = json.loads(event['Records'][0]['body'])

    # DynamoDBに拒絶された登録
    # 環境変数からテーブル名を得る
    table_name = os.environ['dynamo_table']
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table(table_name)

    # ステータスに応答があるかをチェック

    # テーブルから現状ステータスを読み込み
    try:
        response = table.get_item(
            Key={'contact_id': contact_direction['contact_id']},
        )
    except Exception as e:
        print('[Error] DynamoDBから要素の取得に失敗しました。')
        print(e)
        return { 'statusCode': 400 }
    
    # ステータスが変更されていたら終了
    if (response['Item']['status'] != 'inquery'):
        print('既にacceptされています。')
        return { 'statusCode': 200 }
    
    # DynamoDBテーブルに、コンタクトIDとrefuseステータスを書き込み
    try:
        response = table.update_item(
            TableName=table_name,
            Key={'contact_id': contact_direction['contact_id']},
            UpdateExpression = 'set #status = :status',
            ExpressionAttributeNames = {'#status':'status'},
            ExpressionAttributeValues = {':status':'refuse'}
        )
    except Exception as e:
        print('[Error] DynamoDBへの要素の更新に失敗しました。')
        print(e)
        return { 'statusCode': 400 }

    # SNS送信
    sns_body = contact_direction['reciever_name'] + 'さんは今は通話できないようです。折り返し電話が来るのを待ちましょう。'

    sns = boto3.client('sns')
    try:
        response = sns.publish(
            PhoneNumber = contact_direction['caller_phone'],
            Message = sns_body
        )
    except Exception as e:
        print('[Error] SNSのSMS送信(' + contact_direction['caller_phone'] + ')に失敗しました。')
        print(e)
        return { 'statusCode': 400 }

    return { 'statusCode': 200 }

D.Lambda

このLambdaでは、受信者が通話に応答できる場合の処理を行い、API Gatewayでの応答および発信者へのSMSメッセージ送信を行います。
環境変数は以下を設定してください。

  • dynamo_table: DynamoDBテーブルの名前(例: hotline-contact)
  • sqs_url: SQSキューのURL(例: https://sqs.ap-northeast-1.amazonaws.com/999999999999/hotline-sqs)
import json
import boto3
import os

def lambda_handler(event, context):
    print("JSON type event")
    print(json.dumps(event))

    # イベントパラメータからコンタクトIDを得る
    # contactidがあるかどうかもチェックする
    if 'contactid' in event['queryStringParameters']:
        contact_id = event['queryStringParameters']['contactid']
    else:
        print('クエリにcontactid が含まれていません')
        return { 'statusCode': 400 }
        
    if contact_id is None:
        print('イベントのcontactid が無効です')
        return { 'statusCode': 400 }

    # テーブルから現状ステータスをコンタクトIDで検索して読み込み
    # 環境変数からテーブル名を得る
    table_name = os.environ['dynamo_table']
    dynamodb = boto3.resource('dynamodb')
    table = dynamodb.Table(table_name)
    try:
        response = table.get_item(
            Key={'contact_id': contact_id},
        )
    except Exception as e:
        print('[Error] DynamoDBから要素の取得に失敗しました。')
        print(e)
        return { 'statusCode': 400 }

    # コンタクト方向情報の読み込み
    contact_direction = response['Item']['contact_direction']
    status = response['Item']['status']

    # ステータスがrefuseになっていたときのメッセージ作成
    if (response['Item']['status'] != 'inquery'):
        message_to_caller = ( contact_direction['reciever_name'] + 'さんは通話できようになりました。'
                                +contact_direction['reciever_phone'] + ' に電話してください。'
                                )
        message_to_reciever = ( '<font size="14">{0} さんはもう他のことをしているかもしれません。</br>'
                                '折り返すのであればホットラインシステム( {1} )に電話してください。</font>'
                                .format( contact_direction['caller_name'], contact_direction['System_endpoint']['Address'] )
                                )

    # ステータスがinqueryになっていたときのメッセージ作成
    else:
        message_to_caller = ( contact_direction['reciever_name']+'さんは通話できます。 '
                                +contact_direction['reciever_phone']+' に電話してください。')
        message_to_reciever = ( '<font size="14">{0} さんから電話がきます。そのままお待ち下さい。</font>'.format( contact_direction['caller_name'] ) )

    # DynamoDBテーブルに、コンタクトIDとacceptステータスを書き込み
    try:
        response = table.update_item(
            TableName=table_name,
            Key={'contact_id': contact_id},
            UpdateExpression = 'set #status = :status',
            ExpressionAttributeNames = {'#status':'status'},
            ExpressionAttributeValues = {':status':'accept'}
        )
    except Exception as e:
        print('[Error] DynamoDBへの要素の更新に失敗しました。')
        print(e)
        return { 'statusCode': 400 }

    # SNS送信
    sns = boto3.client('sns')

    # 発信者へのSMS送信
    try:
        response_to_caller = sns.publish(
            PhoneNumber = contact_direction['caller_phone'],
            Message = message_to_caller
        )
    except Exception as e:
        print('発信者('+contact_direction['caller_phone']+')へのSMS送信に失敗しました。')
        print(e)
        return { 'statusCode': 400 }

    # 受信者へHTMLでメッセージを表示
    return {
        'statusCode': 200,
        'isBase64Encoded': False,
        'headers': {
            'Content-Type': 'text/html; charset=utf-8'
        },
        'body': message_to_reciever
    }

Amazon Connectインスタンス作成

Amazon Connectインスタンスを作成します。

Connectインスタンスの設定画面(オレンジ管理画面)にLambda関数を登録します。登録するのは、A.LambdaとB.LambdaのARNです。
次に、Connectインスタンスの管理画面(ブルー管理画面)で、電話番号の取得やユーザ作成を行ってください。
その上で、コンタクトフローを新規作成します。以下のリンクの圧縮ファイルをダウンロード・解凍し、コンタクトフローのJSONファイルを取り出してインポートしてください。

インポート後、上の図でオレンジで囲んだLambda呼び出しボックスが2箇所あるので、作成したLambda関数のARNに変更してください。
最後に、「保存」と「公開」を忘れずに。

API Gateway作成

API Gatewayを1つ作成します。 APIタイプはHTTP APIを選択します。
「統合」でLambdaを指定し、D.Lambdaを指定します。

次に、ルートはGETメソッドを指定し、リソースパスは /accept とし、統合ターゲットにD.Lambda関数を指定します。

ここでは、APIの認証については考慮しておりません。

完成・動作確認

これで、設定ファイル phonenumber.json に記載したスマートフォンからAmazon Connectに電話すると、発信者、受信者共に適切にSMSメッセージが届き、電話開始の調整が半自動化できます。

家族や親友や恋人間で使ってみてください。

では、よい電話生活を。

タイトルとURLをコピーしました