Skip to content

Commit e3bfe51

Browse files
author
janligudzinski
committed
add webhook payload type to library
1 parent b5b1b47 commit e3bfe51

File tree

1 file changed

+138
-0
lines changed

1 file changed

+138
-0
lines changed

src/realtime/api/sip.rs

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use serde::{Deserialize, Serialize};
2+
13
use super::*;
24

35
/// Intended for connecting to an already existing Realtime session spawned by accepting an incoming SIP call from e.g. Twilio.
@@ -41,3 +43,139 @@ impl RealtimeSipClient {
4143
Ok((write, read))
4244
}
4345
}
46+
47+
/// This is the payload of a `realtime.call.incoming` event webhook which is what OpenAI sends to your application when a call hits the SIP endpoint for your project.
48+
/// Exposes some convenience methods for when a call comes from Twilio which is one of the more common use cases. `openai_call_id()` is what you will need to use accept/hangup endpoints.
49+
///
50+
/// # Example
51+
/// ```rust
52+
/// const INSTRUCTIONS: &str = "You are a helpful assistant.";
53+
/// #[axum::debug_handler]
54+
/// async fn call_webhook(
55+
/// State(mut state): State<AppState>,
56+
/// Json(event): Json<RealtimeCallIncoming>,
57+
/// ) -> impl IntoResponse {
58+
/// let number = event.caller_number();
59+
/// let call_id = event.openai_call_id();
60+
/// let twilio_sid = event.twilio_call_sid();
61+
/// let account_sid = event.twilio_account_sid();
62+
/// log::info!(
63+
/// "Call coming in from {:?} with OpenAi ID {:?}, Twilio SID {:?} / account SID {:?}",
64+
/// number,
65+
/// call_id,
66+
/// twilio_sid,
67+
/// account_sid
68+
/// );
69+
///
70+
/// let accept_call = AcceptCallRequest::new(INSTRUCTIONS, RealtimeModel::GptRealtime);
71+
///
72+
/// match state.openai_client.accept_call(call_id, accept_call).await {
73+
/// Ok(_) => {
74+
/// log::info!("Accepted call {}", call_id);
75+
/// }
76+
/// Err(err) => {
77+
/// log::error!("Failed to accept call {}: {}", call_id, err);
78+
/// }
79+
/// };
80+
/// ()
81+
/// }
82+
/// ```
83+
#[derive(Debug, Clone, Serialize, Deserialize)]
84+
pub struct RealtimeCallIncoming {
85+
pub id: String,
86+
/// Always `event`.
87+
pub object: String,
88+
pub created_at: i64,
89+
/// This should always be `realtime.call.incoming`.
90+
#[serde(rename = "type")]
91+
pub event_type: String,
92+
/// Contains the actual unique data per call. Look for `call_id` here or call `openai_call_id()`.
93+
pub data: RealTimeCallIncomingData,
94+
}
95+
96+
#[derive(Debug, Clone, Serialize, Deserialize)]
97+
pub struct RealTimeCallIncomingData {
98+
pub call_id: String,
99+
pub sip_headers: Vec<SipHeader>,
100+
}
101+
102+
#[derive(Debug, Clone, Serialize, Deserialize)]
103+
pub struct SipHeader {
104+
pub name: String,
105+
pub value: String,
106+
}
107+
108+
impl RealtimeCallIncoming {
109+
/// Get the call ID from the event data
110+
pub fn openai_call_id(&self) -> &str {
111+
&self.data.call_id
112+
}
113+
114+
/// Extract the caller's phone number from the "From" SIP header
115+
pub fn caller_number(&self) -> Option<String> {
116+
self.data
117+
.sip_headers
118+
.iter()
119+
.find(|header| header.name == "From")
120+
.and_then(|header| {
121+
// Parse the From header to extract the phone number
122+
// Format: "+48797177128" <sip:[email protected]:5060>;tag=...
123+
if let Some(start) = header.value.find('"') {
124+
if let Some(end) = header.value[start + 1..].find('"') {
125+
return Some(header.value[start + 1..start + 1 + end].to_string());
126+
}
127+
}
128+
None
129+
})
130+
}
131+
132+
/// Get the Twilio Call SID from the X-Twilio-CallSid SIP header
133+
pub fn twilio_call_sid(&self) -> Option<&str> {
134+
self.data
135+
.sip_headers
136+
.iter()
137+
.find(|header| header.name == "X-Twilio-CallSid")
138+
.map(|header| header.value.as_str())
139+
}
140+
141+
/// Get the Twilio Account SID from the X-Twilio-AccountSid SIP header
142+
pub fn twilio_account_sid(&self) -> Option<&str> {
143+
self.data
144+
.sip_headers
145+
.iter()
146+
.find(|header| header.name == "X-Twilio-AccountSid")
147+
.map(|header| header.value.as_str())
148+
}
149+
150+
/// Get a specific SIP header value by name
151+
pub fn get_sip_header(&self, name: &str) -> Option<&str> {
152+
self.data
153+
.sip_headers
154+
.iter()
155+
.find(|header| header.name == name)
156+
.map(|header| header.value.as_str())
157+
}
158+
}
159+
160+
#[cfg(test)]
161+
mod tests {
162+
use super::*;
163+
164+
#[test]
165+
fn test_parse_twilio_event() {
166+
let json = r#"{"id": "evt_68bc6828707881908be189456b84cc07", "object": "event", "created_at": 1757177896, "type": "realtime.call.incoming", "data": {"call_id": "rtc_c5b6f97fe96f4c809b78916a9ac15748", "sip_headers": [{"name": "From", "value": "\"+48123123123\" <sip:[email protected]:5060>;tag=82568196_c3356d0b_03f1232a-01cf-4a4a-af25-bac077219d08"}, {"name": "X-Twilio-CallSid", "value": "CA080dd4bebc0320639d7ae33b82e80481"}, {"name": "X-Twilio-AccountSid", "value": "fake_data"}]}}"#;
167+
168+
let event: RealtimeCallIncoming = serde_json::from_str(json).unwrap();
169+
170+
assert_eq!(
171+
event.openai_call_id(),
172+
"rtc_c5b6f97fe96f4c809b78916a9ac15748"
173+
);
174+
assert_eq!(event.caller_number(), Some("+48123123123".to_string()));
175+
assert_eq!(
176+
event.twilio_call_sid(),
177+
Some("CA080dd4bebc0320639d7ae33b82e80481")
178+
);
179+
assert_eq!(event.twilio_account_sid(), Some("fake_data"));
180+
}
181+
}

0 commit comments

Comments
 (0)