Skip to main content

babelforce_manager_sdk/
client.rs

1use std::sync::Arc;
2
3use reqwest::header::{HeaderValue, AUTHORIZATION};
4
5use crate::auth::Auth;
6use crate::error::ManagerError;
7use crate::gen::auth::apis::configuration::Configuration as AuthConfig;
8use crate::gen::manager::apis::configuration::Configuration as ManagerConfig;
9use crate::gen::task_automation::apis::configuration::Configuration as TaskAutomationConfig;
10use crate::gen::task_schedule::apis::configuration::Configuration as TaskScheduleConfig;
11use crate::gen::user::apis::configuration::Configuration as UserConfig;
12use crate::resources::agents::{AgentGroupsResource, AgentsResource};
13use crate::resources::applications::{ApplicationsResource, LocalAutomationsResource};
14use crate::resources::auth::AuthResource;
15use crate::resources::automations::AutomationsResource;
16use crate::resources::babeldesk::{BabeldeskResource, BabeldeskWidgetsResource};
17use crate::resources::businesshours::BusinessHoursResource;
18use crate::resources::calendars::CalendarsResource;
19use crate::resources::calls::{CallsResource, ReportingResource};
20use crate::resources::campaigns::CampaignsResource;
21use crate::resources::conferences::ConferencesResource;
22use crate::resources::conversations::ConversationsResource;
23use crate::resources::dashboards::DashboardsResource;
24use crate::resources::dialer::{DialerBehavioursResource, DialerResource};
25use crate::resources::events::EventsResource;
26use crate::resources::expressions::ExpressionsResource;
27use crate::resources::files::FilesResource;
28use crate::resources::integrations::IntegrationsResource;
29use crate::resources::logs::LogsResource;
30use crate::resources::me::MeResource;
31use crate::resources::metrics::MetricsResource;
32use crate::resources::numbers::NumbersResource;
33use crate::resources::outbound::OutboundResource;
34use crate::resources::phonebook::PhonebookResource;
35use crate::resources::prompts::PromptsResource;
36use crate::resources::queues::{QueueSelectionsResource, QueuesResource};
37use crate::resources::recordings::RecordingsResource;
38use crate::resources::routing::RoutingResource;
39use crate::resources::schedules::TaskSchedulesResource;
40use crate::resources::sessions::SessionsResource;
41use crate::resources::settings::SettingsResource;
42use crate::resources::sms::SmsResource;
43use crate::resources::system::SystemResource;
44use crate::resources::tasks::{
45    TaskMetricsResource, TaskScriptsResource, TaskSecretsResource, TaskSelectionConfigResource,
46    TasksResource,
47};
48use crate::resources::triggers::TriggersResource;
49use crate::resources::users::UsersResource;
50use crate::retry::RetryPolicy;
51
52/// Default babelforce API host. Override with [`ManagerClientBuilder::base_url`].
53pub const DEFAULT_BASE_URL: &str = "https://services.babelforce.com";
54
55/// The babelforce manager SDK client. Build one with [`ManagerClient::connect`] (or
56/// [`ManagerClient::builder`]) and use its resource namespaces.
57pub struct ManagerClient {
58    /// User management — `/api/v2/users`.
59    pub users: UsersResource,
60    /// Agent management — `/api/v2/agents` (with a nested `groups` sub-resource).
61    pub agents: AgentsResource,
62    /// The authenticated principal — current user, accounts, password reset (`/api/v2/user`).
63    pub me: MeResource,
64    /// Call reporting & control — `/api/v2/calls` (with a nested `reporting` sub-resource).
65    pub calls: CallsResource,
66    /// SMS records — `/api/v2/sms`.
67    pub sms: SmsResource,
68    /// Service (phone) numbers — `/api/v2/numbers`.
69    pub numbers: NumbersResource,
70    /// Conferences — `/api/v2/conferences`.
71    pub conferences: ConferencesResource,
72    /// Queues — `/api/v2/queues` (with a nested `selections` sub-resource).
73    pub queues: QueuesResource,
74    /// Call recordings — `/api/v2/recordings` (bulk actions and per-recording flagging).
75    pub recordings: RecordingsResource,
76    /// Routing rules — `/api/v2/routings`.
77    pub routing: RoutingResource,
78    /// Workflow triggers — `/api/v2/triggers`.
79    pub triggers: TriggersResource,
80    /// Global automations (event triggers) — `/api/v2/events/triggers`.
81    pub automations: AutomationsResource,
82    /// Integrations — `/api/v2/integrations`.
83    pub integrations: IntegrationsResource,
84    /// Outbound lists & leads — `/api/v2/outbound/lists`.
85    pub outbound: OutboundResource,
86    /// Phonebook entries — `/api/v2/phonebook`.
87    pub phonebook: PhonebookResource,
88    /// Outbound campaigns — `/api/v2/outbound/campaigns`.
89    pub campaigns: CampaignsResource,
90    /// Business hours — `/api/v2/business-hours`.
91    pub business_hours: BusinessHoursResource,
92    /// Calendars — `/api/v2/calendars`.
93    pub calendars: CalendarsResource,
94    /// Conversations — `/api/v2/conversations` (with events & session variables).
95    pub conversations: ConversationsResource,
96    /// Reporting dashboards — `/api/v2/dashboards` (with per-dashboard user management).
97    pub dashboards: DashboardsResource,
98    /// Dialer runtime & reporting — `/api/v2/dialer` (with a nested `behaviours` sub-resource).
99    pub dialer: DialerResource,
100    /// Call/automation sessions — `/api/v2/sessions`.
101    pub sessions: SessionsResource,
102    /// Audio prompts — `/api/v2/prompts`.
103    pub prompts: PromptsResource,
104    /// Babeldesk dashboards — `/api/v2/babeldesk` (with a nested `widgets` sub-resource).
105    pub babeldesk: BabeldeskResource,
106    /// Event definitions & custom events — `/api/v2/events`.
107    pub events: EventsResource,
108    /// Expressions catalog & evaluator — `/api/v2/expressions`.
109    pub expressions: ExpressionsResource,
110    /// Stored files — `/api/v2/files` (recordings, prompts, backups).
111    pub files: FilesResource,
112    /// Audit & live logs.
113    pub logs: LogsResource,
114    /// Metrics — `/api/v2/metrics`.
115    pub metrics: MetricsResource,
116    /// System & reference data — health checks, server time, timezones, tags, templates.
117    pub system: SystemResource,
118    /// Global settings — `/api/v2/settings`, grouped by scope.
119    pub settings: SettingsResource,
120    /// Applications — `/api/v2/applications` (with a nested `local_automations` sub-resource).
121    pub applications: ApplicationsResource,
122    /// Task automation (v3) — `/api/v3/tasks` (with nested scripts/secrets/selection_config/metrics).
123    pub tasks: TasksResource,
124    /// Task schedules (v3) — `/api/v3/schedules`.
125    pub schedules: TaskSchedulesResource,
126    /// OAuth 2.0 — `/oauth/authorize`, `/oauth/token`, `/oauth/revoke`.
127    pub auth: AuthResource,
128}
129
130impl ManagerClient {
131    /// Connect with default settings (base URL [`DEFAULT_BASE_URL`], retries on).
132    pub async fn connect(auth: Auth) -> Result<Self, ManagerError> {
133        Self::builder(auth).connect().await
134    }
135
136    /// Start building a client to customise the base URL or retry policy.
137    pub fn builder(auth: Auth) -> ManagerClientBuilder {
138        ManagerClientBuilder {
139            base_url: DEFAULT_BASE_URL.to_string(),
140            auth,
141            retry: RetryPolicy::default(),
142        }
143    }
144}
145
146/// Builder for [`ManagerClient`].
147pub struct ManagerClientBuilder {
148    base_url: String,
149    auth: Auth,
150    retry: RetryPolicy,
151}
152
153impl ManagerClientBuilder {
154    /// Override the base URL (e.g. a per-customer host). Defaults to [`DEFAULT_BASE_URL`].
155    pub fn base_url(mut self, url: impl Into<String>) -> Self {
156        self.base_url = url.into();
157        self
158    }
159
160    /// Override the retry policy. Defaults to [`RetryPolicy::default`].
161    pub fn retry(mut self, policy: RetryPolicy) -> Self {
162        self.retry = policy;
163        self
164    }
165
166    /// Build the client, resolving a password grant into a bearer token if needed.
167    pub async fn connect(self) -> Result<ManagerClient, ManagerError> {
168        let base = self.base_url.trim_end_matches('/').to_string();
169
170        let mut headers = self.auth.static_headers();
171        let resolved_token = match &self.auth {
172            Auth::Password { user, pass } => Some(
173                fetch_token(
174                    &base,
175                    &[
176                        ("grant_type", "password"),
177                        ("username", user),
178                        ("password", pass),
179                        ("client_id", "manager"),
180                    ],
181                )
182                .await?,
183            ),
184            Auth::ClientCredentials {
185                client_id,
186                client_secret,
187            } => Some(
188                fetch_token(
189                    &base,
190                    &[
191                        ("grant_type", "client_credentials"),
192                        ("client_id", client_id),
193                        ("client_secret", client_secret),
194                    ],
195                )
196                .await?,
197            ),
198            Auth::RefreshToken {
199                refresh_token,
200                client_id,
201            } => Some(
202                fetch_token(
203                    &base,
204                    &[
205                        ("grant_type", "refresh_token"),
206                        ("refresh_token", refresh_token),
207                        ("client_id", client_id),
208                    ],
209                )
210                .await?,
211            ),
212            Auth::Bearer { .. } => None,
213        };
214        if let Some(token) = resolved_token {
215            headers.insert(
216                AUTHORIZATION,
217                HeaderValue::from_str(&format!("Bearer {token}"))
218                    .map_err(|e| ManagerError::Network(e.to_string()))?,
219            );
220        }
221
222        let http = reqwest::Client::builder()
223            .default_headers(headers)
224            .build()
225            .map_err(|e| ManagerError::Network(e.to_string()))?;
226
227        let user_agent =
228            Some(concat!("babelforce-manager-sdk-rust/", env!("CARGO_PKG_VERSION")).to_string());
229
230        let cfg = Arc::new(ManagerConfig {
231            base_path: base.clone(),
232            user_agent: user_agent.clone(),
233            client: http.clone(),
234            basic_auth: None,
235            oauth_access_token: None,
236            bearer_access_token: None,
237            api_key: None,
238        });
239
240        // The other specs live on the same host and share the auth-configured reqwest client.
241        let user_cfg = Arc::new(UserConfig {
242            base_path: base.clone(),
243            user_agent: user_agent.clone(),
244            client: http.clone(),
245            basic_auth: None,
246            oauth_access_token: None,
247            bearer_access_token: None,
248            api_key: None,
249        });
250        let task_automation_cfg = Arc::new(TaskAutomationConfig {
251            base_path: base.clone(),
252            user_agent: user_agent.clone(),
253            client: http.clone(),
254            basic_auth: None,
255            oauth_access_token: None,
256            bearer_access_token: None,
257            api_key: None,
258        });
259        let task_schedule_cfg = Arc::new(TaskScheduleConfig {
260            base_path: base.clone(),
261            user_agent: user_agent.clone(),
262            client: http.clone(),
263            basic_auth: None,
264            oauth_access_token: None,
265            bearer_access_token: None,
266            api_key: None,
267        });
268        // OAuth endpoints carry their credentials in the request body, but share the same host/client.
269        let auth_cfg = Arc::new(AuthConfig {
270            base_path: base,
271            user_agent,
272            client: http,
273            basic_auth: None,
274            oauth_access_token: None,
275            bearer_access_token: None,
276            api_key: None,
277        });
278
279        Ok(ManagerClient {
280            users: UsersResource {
281                cfg: cfg.clone(),
282                retry: self.retry.clone(),
283            },
284            agents: AgentsResource {
285                cfg: cfg.clone(),
286                retry: self.retry.clone(),
287                groups: AgentGroupsResource {
288                    cfg: cfg.clone(),
289                    retry: self.retry.clone(),
290                },
291            },
292            me: MeResource {
293                cfg: user_cfg,
294                retry: self.retry.clone(),
295            },
296            calls: CallsResource {
297                cfg: cfg.clone(),
298                retry: self.retry.clone(),
299                reporting: ReportingResource {
300                    cfg: cfg.clone(),
301                    retry: self.retry.clone(),
302                },
303            },
304            sms: SmsResource {
305                cfg: cfg.clone(),
306                retry: self.retry.clone(),
307            },
308            numbers: NumbersResource {
309                cfg: cfg.clone(),
310                retry: self.retry.clone(),
311            },
312            conferences: ConferencesResource {
313                cfg: cfg.clone(),
314                retry: self.retry.clone(),
315            },
316            queues: QueuesResource {
317                cfg: cfg.clone(),
318                retry: self.retry.clone(),
319                selections: QueueSelectionsResource {
320                    cfg: cfg.clone(),
321                    retry: self.retry.clone(),
322                },
323            },
324            recordings: RecordingsResource {
325                cfg: cfg.clone(),
326                retry: self.retry.clone(),
327            },
328            routing: RoutingResource {
329                cfg: cfg.clone(),
330                retry: self.retry.clone(),
331            },
332            triggers: TriggersResource {
333                cfg: cfg.clone(),
334                retry: self.retry.clone(),
335            },
336            automations: AutomationsResource {
337                cfg: cfg.clone(),
338                retry: self.retry.clone(),
339            },
340            integrations: IntegrationsResource {
341                cfg: cfg.clone(),
342                retry: self.retry.clone(),
343            },
344            outbound: OutboundResource {
345                cfg: cfg.clone(),
346                retry: self.retry.clone(),
347            },
348            phonebook: PhonebookResource {
349                cfg: cfg.clone(),
350                retry: self.retry.clone(),
351            },
352            campaigns: CampaignsResource {
353                cfg: cfg.clone(),
354                retry: self.retry.clone(),
355            },
356            business_hours: BusinessHoursResource {
357                cfg: cfg.clone(),
358                retry: self.retry.clone(),
359            },
360            calendars: CalendarsResource {
361                cfg: cfg.clone(),
362                retry: self.retry.clone(),
363            },
364            conversations: ConversationsResource {
365                cfg: cfg.clone(),
366                retry: self.retry.clone(),
367            },
368            dashboards: DashboardsResource {
369                cfg: cfg.clone(),
370                retry: self.retry.clone(),
371            },
372            dialer: DialerResource {
373                cfg: cfg.clone(),
374                retry: self.retry.clone(),
375                behaviours: DialerBehavioursResource {
376                    cfg: cfg.clone(),
377                    retry: self.retry.clone(),
378                },
379            },
380            sessions: SessionsResource {
381                cfg: cfg.clone(),
382                retry: self.retry.clone(),
383            },
384            prompts: PromptsResource {
385                cfg: cfg.clone(),
386                retry: self.retry.clone(),
387            },
388            babeldesk: BabeldeskResource {
389                cfg: cfg.clone(),
390                retry: self.retry.clone(),
391                widgets: BabeldeskWidgetsResource {
392                    cfg: cfg.clone(),
393                    retry: self.retry.clone(),
394                },
395            },
396            events: EventsResource {
397                cfg: cfg.clone(),
398                retry: self.retry.clone(),
399            },
400            expressions: ExpressionsResource {
401                cfg: cfg.clone(),
402                retry: self.retry.clone(),
403            },
404            files: FilesResource {
405                cfg: cfg.clone(),
406                retry: self.retry.clone(),
407            },
408            logs: LogsResource {
409                cfg: cfg.clone(),
410                retry: self.retry.clone(),
411            },
412            metrics: MetricsResource {
413                cfg: cfg.clone(),
414                retry: self.retry.clone(),
415            },
416            system: SystemResource {
417                cfg: cfg.clone(),
418                retry: self.retry.clone(),
419            },
420            settings: SettingsResource {
421                cfg: cfg.clone(),
422                retry: self.retry.clone(),
423            },
424            applications: ApplicationsResource {
425                cfg: cfg.clone(),
426                retry: self.retry.clone(),
427                local_automations: LocalAutomationsResource {
428                    cfg: cfg.clone(),
429                    retry: self.retry.clone(),
430                },
431            },
432            tasks: TasksResource {
433                cfg: task_automation_cfg.clone(),
434                retry: self.retry.clone(),
435                scripts: TaskScriptsResource {
436                    cfg: task_automation_cfg.clone(),
437                    retry: self.retry.clone(),
438                },
439                secrets: TaskSecretsResource {
440                    cfg: task_automation_cfg.clone(),
441                    retry: self.retry.clone(),
442                },
443                selection_config: TaskSelectionConfigResource {
444                    cfg: task_automation_cfg.clone(),
445                    retry: self.retry.clone(),
446                },
447                metrics: TaskMetricsResource {
448                    cfg: task_automation_cfg.clone(),
449                    retry: self.retry.clone(),
450                },
451            },
452            schedules: TaskSchedulesResource {
453                cfg: task_schedule_cfg,
454                retry: self.retry.clone(),
455            },
456            auth: AuthResource {
457                cfg: auth_cfg,
458                retry: self.retry.clone(),
459            },
460        })
461    }
462}
463
464/// POST an OAuth2 token request to `{base}/oauth/token` with the given form params (a `password` or
465/// `client_credentials` grant) and return its `access_token`.
466async fn fetch_token(base: &str, params: &[(&str, &str)]) -> Result<String, ManagerError> {
467    #[derive(serde::Deserialize)]
468    struct Token {
469        access_token: String,
470    }
471    let resp = reqwest::Client::new()
472        .post(format!("{base}/oauth/token"))
473        .form(params)
474        .send()
475        .await
476        .map_err(|e| ManagerError::Network(e.to_string()))?;
477    let status = resp.status();
478    let body = resp
479        .text()
480        .await
481        .map_err(|e| ManagerError::Network(e.to_string()))?;
482    if !status.is_success() {
483        return Err(ManagerError::from_response(status.as_u16(), body));
484    }
485    let token: Token =
486        serde_json::from_str(&body).map_err(|e| ManagerError::Decode(e.to_string()))?;
487    Ok(token.access_token)
488}