🤯

ボスケテ・・・Slackに書いた内容を音声に変換して電話してみる

2024/12/20に公開

はじめに

ボスケテ・・・「ボス、決して走らず、急いで歩いてきて。そして僕らを助けて」の意。

長らくおこなってきたデータセンターからパブリッククラウドへの移行もようやく最終段階。最後のサービスも無事に移行し、残るはシステムの停止調整やデータセンターのクローズ作業。最後の輝きを残しながらふんばっているインフラエンジニアです。

この記事は GMOメディア株式会社 Advent Calendar 2024 の20日目の記事です。
https://qiita.com/advent-calendar/2024/gmo-media

普段バリバリ使っているAWSですが、今回は業務ではなかなか触れる機会のないサービスにも挑戦してみることにしました。そこで考えたのが、Slackに書いた内容を音声に変換して電話を掛ける仕組みです。

完成イメージ



Slackのアプリに「電話で発信したい内容」を記載すると、ボスの携帯電話に電話がかかり、内容を音声で伝えてくれます。

全体の流れ

以下のステップで実装します:

  • Amazon Connect の作成
  • Lambda の作成
  • API Gateway の作成
  • Slackのアプリ の作成
  • 動作確認

やってみる

1. Amazon Connect

まずはConnectインスタンスを作成します。
今回ID 管理は「Amazon Connect にユーザーを保存」を選択しました。そしてアクセス URLを指定します。

管理者情報を入力する必要があるので、適切に入力し「次へ」をクリックします。

テレフォニーオプションは、今回は発信のみできれば良いので「発信」のみ許可します。

データストレージの設定はそのままで次へをクリックすると確認画面になり、インスタンスの作成をすることができました。

先ほどのID 管理で指定したアクセス URLへ接続すると管理者ワークスペースを開くことができるので、電話番号の取得をします。(発信する際の番号になります)

種類はフリーダイヤルとDID(直接通話に利用できる各国の電話番号)が選べますが、今回はDIDにしました。
また、日本の番号を取得したかったのですが、すぐには取得できそうになかったため、米国の番号を取得しました。

次にやることは、コンタクトフローの作成です。
左ペインの「ルーティング」-「フロー」をクリックし、「フローを作成」をクリックします。
(「発信ウィスパーフローを作成」など選べますが、普通に「フローを作成」で大丈夫です。)

ここではフローの内容を作成していきます。
Slackに書いた内容を音声にして電話をかけたいので、音声の設定をします。
言語は「日本語」で、音声は「Mizuki」にしました。

その次にプロンプトの再生内容を設定します。
「テキスト読み上げまたはチャットテキスト」の「動的に設定」を選択し、名前空間を「ユーザー定義済み」、キーを「DynamicMessage」に設定します。
(DynamicMessageは後ほどLambdaで定義します。)

プロンプトの再生が完了したらすぐに切断してほしいので、終了イベントに「切断」を選びました。

2. Lambda

関数を一つ作成します。
関数名は「bosukete」、ランタイムは「Python」を選びました。

次にAmazon Connectと連携できるようにIAMロールに下記のポリシーを付与します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "connect:StartOutboundVoiceContact",
            "Resource": "*"
        }
    ]
}

関数内で使用する環境変数を定義していきます。
CONNECT_INSTANCE_ID・・・Amazon ConnectのINSTANCE ID
CONTACT_FLOW_ID・・・Amazon ConnectのFLOW ID
FIXED_PHONE_NUMBER・・・電話をかける相手の電話番号(+81XXXXXXXXXX)
SLACK_SIGNING_SECRET・・・SlackアプリのSIGNING SECRET
SOURCE_PHONE_NUMBER・・・Amazon Connectで取得した電話番号(+1XXXXXXXXXX)

※今回は環境変数で定義しましたが、本来であればAWS Secrets Managerを使用した方が良いかもしれません。また、電話をかける相手はいったん決めうちにしたため、ここでFIXED_PHONE_NUMBERとして定義しています。

※Amazon ConnectのINSTANCE IDはここです

※Amazon ConnectのFLOW IDはここです

まずはこの状態で、電話がかかるかテストしてみましょう。
Lambdaのコードを下記のように書いてテストボタンをクリックして、電話がかかってくれば成功です。

import boto3
import os

# Amazon Connect クライアントの初期化
connect_client = boto3.client('connect')

# 環境変数から設定値を取得
CONNECT_INSTANCE_ID = os.environ['CONNECT_INSTANCE_ID']
CONTACT_FLOW_ID = os.environ['CONTACT_FLOW_ID']
SOURCE_PHONE_NUMBER = os.environ['SOURCE_PHONE_NUMBER']
FIXED_PHONE_NUMBER = os.environ['FIXED_PHONE_NUMBER']

def lambda_handler(event, context):
    # 固定メッセージを設定
    message = "This is a test call from Amazon Connect."

    # Amazon Connect で固定電話番号に発信
    connect_client.start_outbound_voice_contact(
        InstanceId=CONNECT_INSTANCE_ID,
        ContactFlowId=CONTACT_FLOW_ID,
        DestinationPhoneNumber=FIXED_PHONE_NUMBER,
        SourcePhoneNumber=SOURCE_PHONE_NUMBER,
        Attributes={'DynamicMessage': message}
    )

    return {
        "statusCode": 200,
        "body": "Outbound call initiated successfully!"
    }

こんな感じで電話がかかってくるので、ドキッとします。
messageの内容もちゃんと話してくれています。

3. API Gateway

Slack等からLambdaを呼び出すためのAPI Gatewayを作成します。
今回はREST APIで、API エンドポイントタイプは「リージョン」を選択しました。

後ほどSlackと連携する際に、Signing Secretを使ってSlackからのリクエストを検証しようと思うので、メソッドリクエストのHTTP リクエストヘッダーに下記の設定を入れておきます。

  • X-Slack-Request-Timestamp
  • X-Slack-Signature

また、Slackから接続されるため、統合リクエストのマッピングテンプレートに下記を設定します。

  • application/x-www-form-urlencoded
{
    "body": "$input.body",
    "headers": {
        "X-Slack-Request-Timestamp": "$input.params('X-Slack-Request-Timestamp')",
        "X-Slack-Signature": "$input.params('X-Slack-Signature')"
    }
}

設定を変更したら忘れずにデプロイしましょう。

4. Slackのアプリ

https://api.slack.com/apps/ の「Create New App」から新しいアプリを作成します。
「From a manifest」や「From scratch」を選べるところは「From scratch」でかまいません。
アプリの名前やワークスペースを設定したらCreate Appします。

Basic InformationでSigning Secretの値を表示させコピーして、さきほど作成したLambdaの環境変数「SLACK_SIGNING_SECRET」に設定します。

次にSlash Commandsで、CommandやRequest URL、Short Descriptionなどを設定します。

※Request URLはこれです

最後にInstall Appで、このSlackアプリを使用できるようにします。

ここまできたらLambdaの関数を下記のように修正します。

import os
import json
import boto3
import hmac
import hashlib
import time
from urllib.parse import parse_qs

# Amazon Connect クライアントの初期化
connect_client = boto3.client('connect')

# 環境変数から設定値を取得
CONNECT_INSTANCE_ID = os.environ['CONNECT_INSTANCE_ID']
CONTACT_FLOW_ID = os.environ['CONTACT_FLOW_ID']
SOURCE_PHONE_NUMBER = os.environ['SOURCE_PHONE_NUMBER']
FIXED_PHONE_NUMBER = os.environ['FIXED_PHONE_NUMBER']
SLACK_SIGNING_SECRET = os.environ['SLACK_SIGNING_SECRET']

# Slack署名の検証関数
def verify_slack_signature(headers, body):
    timestamp = headers.get('x-slack-request-timestamp', '')
    if not timestamp or abs(time.time() - int(timestamp)) > 300:
        raise ValueError("Invalid or expired Slack timestamp")

    sig_basestring = f"v0:{timestamp}:{body}"
    my_signature = 'v0=' + hmac.new(
        SLACK_SIGNING_SECRET.encode(),
        sig_basestring.encode(),
        hashlib.sha256
    ).hexdigest()

    slack_signature = headers.get('x-slack-signature', '')
    if not slack_signature or not hmac.compare_digest(my_signature, slack_signature):
        raise ValueError("Invalid Slack Signature")

def lambda_handler(event, context):
    try:
        # Slackからのリクエストヘッダーとボディを取得
        headers = {k.lower(): v for k, v in event.get('headers', {}).items()}
        body = event.get('body', '')

        # Slack署名の検証
        verify_slack_signature(headers, body)

        # ボディからメッセージを取得
        parsed_body = parse_qs(body)
        message = parsed_body.get('text', [''])[0].strip()

        if not message:
            raise ValueError("Message content is missing")

        # Amazon Connectで固定電話番号に発信
        connect_client.start_outbound_voice_contact(
            InstanceId=CONNECT_INSTANCE_ID,
            ContactFlowId=CONTACT_FLOW_ID,
            DestinationPhoneNumber=FIXED_PHONE_NUMBER,
            SourcePhoneNumber=SOURCE_PHONE_NUMBER,
            Attributes={'DynamicMessage': message}
        )

        # Slackへのレスポンス
        return {
            "response_type": "ephemeral",
            "text": "📞 発信を開始しました!"
        }

    except Exception as e:
        print(f"Error: {str(e)}")
        return {
            "response_type": "ephemeral",
            "text": f"⚠️ エラー: {str(e)}"
        }

5. 動作確認

最後に動作確認をして問題なく電話が発信されれば完成です。

※こんなイタズラをしてはいけません

まとめ

今回は普段触れる機会のないサービスに挑戦してみました。内容に関しては温かい目で見ていただければ幸いです。そして、同じようなことをしてみたい方の少しでも参考になるところがあったなら幸いです。

GMOメディアテックブログ

Discussion