active_call/useragent/
webhook.rs1use crate::{call::RoutingState, useragent::invitation::InvitationHandler};
2use anyhow::Result;
3use async_trait::async_trait;
4use chrono::Utc;
5use reqwest::Client;
6use rsip::prelude::{HasHeaders, HeadersExt};
7use rsipstack::dialog::server_dialog::ServerInviteDialog;
8use serde_json::json;
9use std::{sync::Arc, time::Instant};
10use tokio_util::sync::CancellationToken;
11use tracing::info;
12
13pub struct WebhookInvitationHandler {
14 urls: Vec<String>,
15 method: Option<String>,
16 headers: Option<Vec<(String, String)>>,
17}
18
19impl WebhookInvitationHandler {
20 pub fn new(
21 urls: Vec<String>,
22 method: Option<String>,
23 headers: Option<Vec<(String, String)>>,
24 ) -> Self {
25 Self {
26 urls,
27 method,
28 headers,
29 }
30 }
31}
32
33#[async_trait]
34impl InvitationHandler for WebhookInvitationHandler {
35 async fn on_invite(
36 &self,
37 dialog_id: String,
38 _cancel_token: CancellationToken,
39 dialog: ServerInviteDialog,
40 routing_state: Arc<RoutingState>,
41 ) -> Result<()> {
42 let client = Client::new();
43 let create_time = Utc::now().to_rfc3339();
44
45 let invite_request = dialog.initial_request();
46 let caller = invite_request.from_header()?.uri()?.to_string();
47 let callee = invite_request.to_header()?.uri()?.to_string();
48 let headers = invite_request
49 .headers()
50 .clone()
51 .into_iter()
52 .map(|h| h.to_string())
53 .collect::<Vec<_>>();
54
55 let payload = json!({
56 "dialogId": dialog_id,
57 "createdAt": create_time,
58 "caller": caller,
59 "callee": callee,
60 "event": "invite",
61 "headers": headers,
62 "offer": String::from_utf8_lossy(invite_request.body()),
63 });
64 let idx = routing_state.next_round_robin_index("useragent_webhook", self.urls.len());
67 let url = match self.urls.get(idx) {
68 Some(u) => u,
69 None => {
70 return Err(anyhow::anyhow!("no webhook URL configured"));
71 }
72 };
73
74 let method = self.method.as_deref().unwrap_or("POST");
75 let mut request = client.request(reqwest::Method::from_bytes(method.as_bytes())?, url);
76
77 if let Some(headers) = &self.headers {
78 for (key, value) in headers {
79 request = request.header(key, value);
80 }
81 }
82
83 let start_time = Instant::now();
84 match request.json(&payload).send().await {
85 Ok(response) => {
86 info!(
87 dialog_id,
88 url,
89 caller,
90 callee,
91 elapsed = start_time.elapsed().as_millis(),
92 status = ?response.status(),
93 "invite to webhook"
94 );
95 if !response.status().is_success() {
96 return Err(anyhow::anyhow!("failed to send invite to webhook"));
97 }
98 }
99 Err(e) => {
100 return Err(anyhow::anyhow!("failed to send invite to webhook: {}", e));
101 }
102 }
103 Ok(())
104 }
105}