はじめに#
こんにちは。and factory でバックエンドエンジニアをしている江州です。
Twilioを使ってGoで通話機能を実装する方法を調べたとき、公式ドキュメントは充実しているもののNode.jsやPythonの例が多く、Goでの実装例はまだ少ないと感じました。
この記事では、Twilioの公式Go SDK(twilio-go)を使い、Conference(会議室)ベースの通話システムを構築するパターンを紹介します。単純な1対1の電話だけでなく、通話中のアナウンス・DTMF認証・VoIP/PSTN両対応・録音といった実用的な要素をカバーします。
対象読者#
- GoでTwilioを使った通話機能を構築しようとしている方
- TwilioのConference / TwiML / VoIPの組み合わせを知りたい方
- twilio-go SDKの具体的な使い方を知りたい方
動作確認環境#
| 種別 | バージョン |
|---|
| Go | 1.24 |
| twilio-go | v1 系 |
1. twilio-go SDK のセットアップ#
Twilioの公式Go SDK twilio-go をインストールします。
1
| go get github.com/twilio/twilio-go
|
REST APIクライアントの作成は次のとおりです。
1
2
3
4
5
6
| import "github.com/twilio/twilio-go"
client := twilio.NewRestClientWithParams(twilio.ClientParams{
Username: "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", // Account SID
Password: "your_auth_token", // Auth Token
})
|
環境変数 TWILIO_ACCOUNT_SID と TWILIO_AUTH_TOKEN を設定しておけば、引数なしの twilio.NewRestClient() でもクライアントを作成できます。
2. なぜ Conference(会議室)ベースなのか#
Twilioで2者間の通話を実現するには、主に2つのアプローチがあります。
| 方式 | 概要 | 特徴 |
|---|
<Dial> 直接接続 | 発信者から直接相手に接続 | シンプルだが、通話中の制御が限定的 |
<Conference> | 仮想会議室に参加者を合流 | 通話中アナウンス・録音・参加者管理が柔軟 |
単純な転送や直接通話なら <Dial> で十分です。しかし通話中のアナウンスや会議室単位の録音、参加者の入退室トラッキングが必要であればConference一択です。
Conference の基本フロー#
sequenceDiagram
participant A as 参加者A
participant T as Twilio
participant S as サーバー
participant B as 参加者B
A->>T: 電話をかける
T->>S: Webhook(着信)
S->>T: TwiML: 会議室Xに入室(保留音で待機)
S->>T: MakeCall(参加者Bに発信)
T->>B: 着信
B->>T: 応答
T->>S: Webhook(応答)
S->>T: TwiML: 会議室Xに入室(録音開始)
Note over A,B: 会議室Xで通話開始
参加者Aが先に会議室へ入って保留音を聞きながら待ち、参加者Bが入室した時点で通話が始まります。
3. TwiML生成をGoで型安全に書く#
TwiML(Twilio Markup Language)は、Twilioに通話の制御方法を指示するXMLです。twilio-go SDKには twiml パッケージが含まれており、XMLを文字列で組み立てる必要がありません。
基本: VoiceSay#
1
2
3
4
5
6
7
8
9
10
| import "github.com/twilio/twilio-go/twiml"
func sayXML(message string) (string, error) {
say := &twiml.VoiceSay{
Language: "ja-JP",
Voice: "Polly.Mizuki", // Amazon Polly の日本語音声
Message: message,
}
return twiml.Voice([]twiml.Element{say})
}
|
twiml.Voice() に twiml.Element のスライスを渡すと、TwiMLのXML文字列が生成されます。上の例では次のようなXMLになります。
1
2
3
4
| <?xml version="1.0" encoding="UTF-8"?>
<Response>
<Say language="ja-JP" voice="Polly.Mizuki">こんにちは</Say>
</Response>
|
Conference に入室させるTwiML#
参加者を会議室に入室させるTwiMLの例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| func joinConferenceXML(roomName, statusCallbackURL string) (string, error) {
say := &twiml.VoiceSay{
Language: "ja-JP",
Voice: "Polly.Mizuki",
Message: "担当者にお繋ぎしますので、少々お待ちください。",
}
conference := &twiml.VoiceConference{
Name: roomName,
StartConferenceOnEnter: "false",
EndConferenceOnExit: "true",
StatusCallbackEvent: "start end join leave",
StatusCallback: statusCallbackURL,
StatusCallbackMethod: "POST",
WaitUrl: "http://com.twilio.music.classical.s3.amazonaws.com/ith_brahms-116-4.mp3",
WaitMethod: "GET",
}
dial := &twiml.VoiceDial{
InnerElements: []twiml.Element{conference},
}
return twiml.Voice([]twiml.Element{say, dial})
}
|
重要なパラメータの解説です。
| パラメータ | 値 | 説明 |
|---|
StartConferenceOnEnter | "false" | この参加者の入室では会議を開始しない(待機側に設定) |
EndConferenceOnExit | "true" | この参加者が退室したら会議終了 |
StatusCallbackEvent | "start end join leave" | 受け取りたいイベントを指定 |
WaitUrl | (Twilioの保留音URL) | 会議開始まで再生される保留音 |
もう一方の参加者(会議を開始する側)では StartConferenceOnEnter を "true" にします。
1
2
3
4
5
6
7
| conference := &twiml.VoiceConference{
Name: roomName,
StartConferenceOnEnter: "true",
EndConferenceOnExit: "true",
Record: "record-from-start",
Trim: "do-not-trim",
}
|
TwiML生成のベストプラクティス#
TwiMLは通話のフェーズごとに異なるXMLが必要になります。1つの関数で条件分岐するのではなく、フェーズごとに独立した関数を作ると見通しがよくなります。
1
2
3
4
5
| func waitInConferenceXML(roomName string) (string, error) { ... } // 待機側入室
func startConferenceXML(roomName string) (string, error) { ... } // 開始側入室(録音付き)
func gatherPasswordXML(actionURL string) (string, error) { ... } // パスワード入力要求
func callEndReminderXML(minutes int) (string, error) { ... } // 残り時間アナウンス
func hangupWithMessageXML(message string) (string, error) { ... } // メッセージ後切断
|
4. 発信と通話更新#
MakeCall: 発信#
twilio-goでの発信は Api.CreateCall を使います。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import api "github.com/twilio/twilio-go/rest/api/v2010"
params := &api.CreateCallParams{}
params.SetTo("+819012345678")
params.SetFrom("+815012345678")
params.SetUrl("https://example.com/twiml/answer") // 応答時のTwiML URL
params.SetStatusCallback("https://example.com/callback/status")
params.SetStatusCallbackEvent([]string{"answered", "completed"})
params.SetTimeout(45) // 着信タイムアウト(秒)
resp, err := client.Api.CreateCall(params)
if err != nil {
log.Fatal(err)
}
fmt.Println("CallSid:", *resp.Sid)
|
Url に指定したエンドポイントは、相手が応答したときにTwilioがGETまたはPOSTするWebhookです。ここでConference入室用のTwiMLを返せば、自動的に会議室に参加させられます。
TwiML パラメータを使えば、URLの代わりにTwiML文字列を直接渡すこともできます。
1
2
| xml, _ := joinConferenceXML("room-123", callbackURL)
params.SetTwiml(xml) // Urlの代わりにTwiMLを直接指定
|
UpdateCall: 通話中の制御#
進行中の通話に対して、TwiMLの差し替えや切断指示を送れます。
1
2
3
4
5
6
7
8
9
10
| params := &api.UpdateCallParams{}
// パターン1: 新しいTwiMLを適用(アナウンス後に切断など)
xml, _ := hangupWithMessageXML("まもなく通話が終了します。")
params.SetTwiml(xml)
// パターン2: 強制切断
// params.SetStatus("completed")
resp, err := client.Api.UpdateCall("CAxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", params)
|
SetTwiml でTwiMLを差し替えると、現在再生中の音声やConferenceが中断され、新しいTwiMLの実行に切り替わります。終了案内を流してから切断したい場合に便利です。
UpdateConference: 会議室アナウンス#
Conference参加者全員にアナウンスを流すには UpdateConference を使います。
1
2
3
4
5
| params := &api.UpdateConferenceParams{}
params.SetAnnounceUrl("https://example.com/twiml/reminder")
params.SetAnnounceMethod("POST")
_, err := client.Api.UpdateConference("CFxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", params)
|
AnnounceUrl に指定したエンドポイントが返すTwiMLの <Say> が全参加者に再生されます。通話を中断せずにアナウンスだけ割り込ませられるのがポイントです。
5. DTMF によるパスワード認証#
通話参加前にDTMFトーン(プッシュボタン音)で認証する仕組みは <Gather> で実現できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| func gatherPasswordXML(actionURL string) (string, error) {
say := &twiml.VoiceSay{
Language: "ja-JP",
Voice: "Polly.Mizuki",
Message: "4桁のパスワードを入力してください。",
}
gather := &twiml.VoiceGather{
Input: "dtmf",
Timeout: "30",
NumDigits: "4",
Action: actionURL, // 入力結果の送信先
Method: "POST",
InnerElements: []twiml.Element{say},
}
noInputSay := &twiml.VoiceSay{
Language: "ja-JP",
Voice: "Polly.Mizuki",
Message: "入力がありませんでした。通話を終了します。",
}
hangup := &twiml.VoiceHangup{}
return twiml.Voice([]twiml.Element{gather, noInputSay, hangup})
}
|
<Gather> の動作は次のとおりです。
InnerElements のガイダンスを再生しながらDTMF入力を待つ- 指定桁数の入力があると
Action URLにPOST(入力値は Digits パラメータ) Timeout 秒間入力がなければ <Gather> の後の要素に進む
Action URLのハンドラ側で入力値を検証し、正しければConference入室用のTwiMLを返し、間違っていればリトライ用のTwiMLを返します。パスワードの比較には crypto/subtle の ConstantTimeCompare を使い、タイミング攻撃を防ぎます。また、試行回数の上限を設けて総当たりを防ぐことが重要です(4桁PINは10,000通りしかないため、制限なしだと1回の通話で全パターンを試せてしまいます)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| import "crypto/subtle"
const maxPasswordAttempts = 3
func handlePasswordCallback(w http.ResponseWriter, r *http.Request) {
digits := r.FormValue("Digits")
attempt := getAttemptCount(r) // 1始まり。クエリパラメータやセッションで管理
var xml string
if len(expectedPassword) == 0 {
xml, _ = hangupWithMessageXML("システムエラーが発生しました。")
} else if subtle.ConstantTimeCompare([]byte(digits), []byte(expectedPassword)) == 1 {
xml, _ = startConferenceXML("room-123")
} else if attempt >= maxPasswordAttempts {
xml, _ = hangupWithMessageXML("認証に失敗しました。通話を終了します。")
} else {
xml, _ = gatherPasswordRetryXML(actionURL, attempt+1)
}
w.Header().Set("Content-Type", "application/xml")
w.Write([]byte(xml))
}
|
6. VoIP と PSTN の両対応#
Twilioは電話回線(PSTN)だけでなく、モバイルアプリからのVoIP通話もサポートしています。
PSTN(電話番号)への発信#
通常の電話番号にかける場合は、To にE.164形式の電話番号を指定します。
1
| params.SetTo("+819012345678")
|
PSTN向けには**AMD(Answering Machine Detection)**の有効化を検討しましょう。留守番電話への接続を検知して無駄な通話料を抑えられます。
1
| params.SetMachineDetection("Enable")
|
ただしAMDにはトレードオフがあります。AMD自体が有料オプションで、応答した通話1件ごとに追加課金が発生します。また既定の同期モード(AsyncAmd=false)では判定完了までTwilioが通話をブロックするため、接続にラグが乗ります。コストと体験のバランスを考慮して導入を判断してください。
VoIP(アプリ)への発信#
モバイルアプリのTwilio Voice SDKに向けて発信する場合は、To に client: プレフィックス付きのidentityを指定します。
1
| params.SetTo("client:user-12345")
|
アプリ側がVoIP通話を受けるためには、Access Token(JWT)が必要です。twilio-goのJWTパッケージで発行できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| import twilioJwt "github.com/twilio/twilio-go/client/jwt"
accessToken := twilioJwt.CreateAccessToken(twilioJwt.AccessTokenParams{
AccountSid: accountSid,
SigningKeySid: apiKeySid,
Secret: apiSecret,
Identity: "user-12345",
Ttl: 3600, // トークン有効期間(秒)
})
accessToken.AddGrant(&twilioJwt.VoiceGrant{
Outgoing: twilioJwt.Outgoing{
ApplicationSid: twimlAppSid,
},
Incoming: twilioJwt.Incoming{
Allow: true,
},
PushCredentialSid: pushCredentialSid, // APNs or FCM
})
jwtToken, err := accessToken.ToJwt()
|
| パラメータ | 説明 |
|---|
SigningKeySid / Secret | Twilio ConsoleのAPI Keysで発行 |
ApplicationSid | TwiML Appの SID(発信時のWebhook設定) |
PushCredentialSid | プッシュ通知用の認証情報SID(iOS APNs / Android FCM) |
iOSとAndroidでプッシュ通知の仕組みが異なるため、PushCredentialSid はプラットフォームごとに切り替える必要があります。
VoIP vs PSTN の判定#
発信先がVoIPかPSTNかで処理を分けたい場合、client: プレフィックスの有無で判定できます。
1
2
3
4
5
6
| if strings.HasPrefix(to, "client:") {
// VoIP: AMDは不要
} else {
// PSTN: AMD有効化を検討
params.SetMachineDetection("Enable")
}
|
電話番号のE.164変換#
Twilio APIはE.164形式(+81 + 番号)を要求します。日本の国内番号からの変換は単純な文字列操作で実現できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
| var (
reNonDigit = regexp.MustCompile(`\D`)
reE164 = regexp.MustCompile(`^\+[1-9]\d{1,14}$`)
)
func toE164Japan(phoneNumber string) (string, error) {
// 既にE.164形式なら検証して素通し
if strings.HasPrefix(phoneNumber, "+") {
if !reE164.MatchString(phoneNumber) {
return "", fmt.Errorf("無効なE.164番号: %s", phoneNumber)
}
return phoneNumber, nil
}
// ハイフンなどの非数字を除去
cleaned := reNonDigit.ReplaceAllString(phoneNumber, "")
// 国内番号の先頭が0であることを確認
if len(cleaned) == 0 || cleaned[0] != '0' {
return "", fmt.Errorf("無効な国内電話番号: %s", phoneNumber)
}
// 国内番号は10桁(固定)または11桁(携帯)
if len(cleaned) < 10 || len(cleaned) > 11 {
return "", fmt.Errorf("無効な桁数の電話番号: %s", phoneNumber)
}
// 先頭の0を+81に置換
return "+81" + cleaned[1:], nil
}
// "090-1234-5678" → "+819012345678"
// "+819012345678" → "+819012345678"(そのまま返す)
|
ユーザーへの表示時にはE.164を国内形式に戻します。
1
2
3
4
5
6
7
8
| func toLocalFormat(e164 string) string {
if strings.HasPrefix(e164, "+81") {
return "0" + e164[3:]
}
return e164
}
// "+819012345678" → "09012345678"
|
7. 通話録音#
Conferenceの録音機能を使えば、通話内容を自動的に録音できます。
録音の設定#
Conferenceに入室するTwiMLで録音パラメータを指定します。
1
2
3
4
5
6
7
8
9
| conference := &twiml.VoiceConference{
Name: roomName,
StartConferenceOnEnter: "true",
Record: "record-from-start",
Trim: "do-not-trim",
RecordingStatusCallback: "https://example.com/callback/recording",
RecordingStatusCallbackMethod: "POST",
RecordingStatusCallbackEvent: "completed",
}
|
| パラメータ | 推奨値 | 理由 |
|---|
Record | record-from-start | 会議開始と同時に録音開始 |
Trim | do-not-trim | 無音部分も含めて完全に録音(証跡用途の場合) |
RecordingStatusCallbackEvent | completed | 録音完了時にのみコールバック |
Record は待機側(StartConferenceOnEnter: "false")ではなく、会議を開始する側のConference要素に設定します。
録音ファイルのダウンロード#
録音が完了すると、RecordingStatusCallback に録音の情報がPOSTされます。RecordingUrl パラメータに録音ファイルのURLが含まれるので、そこからダウンロードします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| func downloadRecording(recordingURL, accountSid, authToken string) (io.ReadCloser, error) {
// SSRF対策: Twilio APIのURLであることを検証
if err := validateRecordingURL(recordingURL); err != nil {
return nil, err
}
// MP3形式でデュアルチャンネル(話者分離)ダウンロード
downloadURL := recordingURL + ".mp3?RequestedChannels=2"
req, err := http.NewRequest(http.MethodGet, downloadURL, nil)
if err != nil {
return nil, err
}
req.SetBasicAuth(accountSid, authToken)
client := &http.Client{
Timeout: 5 * time.Minute,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
if len(via) >= 10 {
return fmt.Errorf("リダイレクトが多すぎます")
}
return validateRecordingURL(req.URL.String())
},
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, fmt.Errorf("録音ダウンロード失敗: status=%d", resp.StatusCode)
}
return resp.Body, nil
}
|
- 拡張子(
.mp3)を付けることでフォーマットを指定(デフォルトはWAV) RequestedChannels=2 でデュアルチャンネル(左右で話者が分離)を取得できる。ただしConference録音ではTwilio ConsoleのVoice Recording settingsで事前にDual-channel設定を有効化しておくこと。未有効化で指定すると400が返る- Twilio APIへのリクエストにはBasic認証が必要
CheckRedirect でリダイレクト先のホストも検証し、SSRF対策をリダイレクトチェーンにも適用- 長時間録音はサイズが大きくなるため、タイムアウトを長めに設定
SSRF対策#
録音のコールバックで受け取った RecordingUrl をそのままHTTPリクエストに使う場合、SSRF(Server-Side Request Forgery) に注意が必要です。万が一コールバックが改ざんされた場合、内部ネットワークへのアクセスに悪用される可能性があります。
対策として、URLが https://api.twilio.com/ で始まることを検証します。
1
2
3
4
5
6
7
8
| const twilioAPIBaseURL = "https://api.twilio.com/"
func validateRecordingURL(url string) error {
if !strings.HasPrefix(url, twilioAPIBaseURL) {
return fmt.Errorf("不正なRecordingURL: Twilio APIのURLではありません")
}
return nil
}
|
8. ステータスコールバック#
Twilioは通話やConferenceの状態が変化するたびにWebhookを送信します。これを使ってバックエンド側で通話状態をトラッキングできます。
通話ステータス#
MakeCall 時に StatusCallback と StatusCallbackEvent を設定すると、指定したイベントでコールバックが届きます。
| イベント | タイミング |
|---|
initiated | 発信開始 |
ringing | 相手が着信中 |
answered | 相手が応答 |
completed | 通話終了 |
すべてのイベントを受け取ることもできますが、実際の業務ロジックで必要なのは answered と completed だけというケースが多いです。不要なコールバックを減らすことで、Webhookエンドポイントの負荷を軽減できます。
Conference イベント#
Conferenceのステータスコールバックでは、通話ステータスとは別のイベントが使われます。
| イベント | 用途例 |
|---|
start | 会議開始 → 通話開始時刻の記録 |
end | 会議終了 → 通話終了処理、課金計算 |
join | 参加者入室 → もう一方の参加者への発信トリガー |
leave | 参加者退室 → もう一方の参加者への通知 |
特に join イベントは有用です。PSTNユーザーの場合、ユーザーが実際に電話を取ってConferenceに入室(join)してからもう一方の参加者に発信することで、通話確立前に相手を呼び出してしまう問題を回避できます。
1
2
3
4
5
| // StatusCallbackEventに "join" を含めると、参加者入室時にコールバックが届く
conference := &twiml.VoiceConference{
StatusCallbackEvent: "start end join leave",
StatusCallback: callbackURL,
}
|
9. Webhookの署名検証#
SSRF対策と並んで重要なのが、Webhookリクエスト自体の正当性検証です。TwilioはすべてのWebhookリクエストに X-Twilio-Signature ヘッダを付与しています。twilio-goの RequestValidator を使えば、Auth Tokenでリクエストの正当性を検証できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| import "github.com/twilio/twilio-go/client"
func validateTwilioRequest(r *http.Request, baseURL, authToken string) bool {
requestValidator := client.NewRequestValidator(authToken)
fullURL := baseURL + r.URL.RequestURI()
params := make(map[string]string)
if err := r.ParseForm(); err != nil {
return false
}
for key, values := range r.PostForm {
params[key] = values[0]
}
signature := r.Header.Get("X-Twilio-Signature")
return requestValidator.Validate(fullURL, params, signature)
}
|
baseURL はアプリケーションの設定値(例: https://api.example.com)から渡します。Twilioが署名を生成する際のURLと完全一致する必要があるため、ハードコードではなくTwilio Consoleに設定したものと同じ値を使ってください。なお r.URL.Path ではなく r.URL.RequestURI() を使っているのは、クエリ文字列やpercent-encodingを保持するためです。Path はクエリを含まずURLデコードも行うため、署名が一致しなくなります。
この検証をミドルウェアに組み込めば、Twilio以外からの偽リクエストを排除できます。ステータスコールバック・録音コールバック・DTMF入力コールバックなど、すべてのTwilio Webhookエンドポイントに適用しましょう。
10. 認証情報の管理#
Twilioの認証情報(Account SID / Auth Token / API Key)は厳重に管理する必要があります。
環境別の設定#
開発・ステージング・本番で異なるTwilioアカウントや電話番号を使う場合の管理方法です。
1
2
3
4
5
6
7
8
9
10
| type TwilioConfig struct {
AccountSID string
AuthToken string
PhoneNumber string
TwiMLAppSID string
APIKeySID string
APISecret string
PushCredentialSIDiOS string
PushCredentialSIDFCM string
}
|
- ローカル開発: 環境変数または
.env ファイル - ステージング/本番: AWS SSM Parameter StoreやGCP Secret Managerなどのシークレット管理サービス
いずれの場合も、AccountSIDやAuthTokenをソースコードや設定ファイルにハードコードしないことが鉄則です。
Twilio テスト用のクレデンシャル#
Twilioはテスト用のクレデンシャルを提供しています。テスト用のAccount SIDとAuth Tokenを使えば、実際の通話は発生せず、課金もされません。自動テストではこれを活用しましょう。
11. 実装上のTips#
タイムアウトの設定#
MakeCall のデフォルトタイムアウトは60秒ですが、ユースケースに応じて短くすることを検討しましょう。
60秒だと留守電が応答してしまうケースがあります。45秒程度に設定すれば、ほとんどのキャリアの留守電よりも先にタイムアウトします。
TwiMLの前後に Pause を入れる#
音声ガイダンスの前後に短いPauseを入れると、聞き取りやすくなります。
1
2
3
4
5
6
7
| func voiceWithPause(elements ...twiml.Element) (string, error) {
all := make([]twiml.Element, 0, len(elements)+2)
all = append(all, &twiml.VoicePause{Length: "1"})
all = append(all, elements...)
all = append(all, &twiml.VoicePause{Length: "1"})
return twiml.Voice(all)
}
|
電話が繋がった直後や切断直前は回線のノイズが入りやすいため、1秒のPauseがあるだけでガイダンスの頭切れを防げます。
Conference の Trim 設定に注意#
Twilioのデフォルトでは録音の先頭・末尾の無音がトリムされます。通常の用途では問題ありませんが、証跡として完全な録音が必要な場合は do-not-trim を明示的に指定してください。
HTTP クライアントのタイムアウト#
録音ダウンロードなど、レスポンスに時間がかかるTwilio APIコールでは、GoのHTTPクライアントのタイムアウトをデフォルト(無制限)から適切な値に設定しましょう。
1
| client := &http.Client{Timeout: 5 * time.Minute}
|
長時間通話の録音ファイルは数十MBになることもあるため、短すぎるタイムアウトではダウンロードが途中で切れます。
まとめ#
GoとTwilioで通話システムを構築する際のポイントを振り返ります。
- Conference ベースで構築すると、通話中アナウンス・録音・参加者イベント管理が柔軟にできる
- twilio-goのtwimlパッケージを使えば、TwiMLを型安全に生成できる。用途別に関数を分離すると見通しがよい
- VoIPとPSTNは発信方式(
client: vs E.164)、AMD、プッシュ通知など差異があるため、それぞれの特性を理解して実装する - 録音ダウンロード時のSSRF対策はURLプレフィックス検証で行う
- Webhookの署名検証(
X-Twilio-Signature)で偽リクエストを排除する - ステータスコールバックは必要なイベントだけ選択し、特にConferenceの
join イベントはフロー制御に活用できる - 認証情報はシークレット管理サービスで管理し、コードにハードコードしない
twilio-go SDKはTwilioのREST APIをほぼ完全にカバーしており、Goからでも不自由なくTwilioの機能を活用できます。この記事がGoでの通話機能開発の参考になれば幸いです。
関連リンク#