1use std::future::Future;
7
8use reqwest::header::{HeaderMap, HeaderValue};
9use secrecy::ExposeSecret;
10use serde::Serialize;
11use serde::de::DeserializeOwned;
12use tracing::debug;
13use url::Url;
14use uuid::Uuid;
15
16use super::types;
17use crate::Error;
18
19#[derive(serde::Deserialize)]
22struct ErrorResponse {
23 #[serde(default)]
24 message: Option<String>,
25 #[serde(default)]
26 code: Option<String>,
27}
28
29pub struct IntegrationClient {
36 http: reqwest::Client,
37 base_url: Url,
38}
39
40impl IntegrationClient {
41 pub fn from_api_key(
49 base_url: &str,
50 api_key: &secrecy::SecretString,
51 transport: &crate::TransportConfig,
52 platform: crate::ControllerPlatform,
53 ) -> Result<Self, Error> {
54 let mut headers = HeaderMap::new();
55 let mut key_value =
56 HeaderValue::from_str(api_key.expose_secret()).map_err(|e| Error::Authentication {
57 message: format!("invalid API key header value: {e}"),
58 })?;
59 key_value.set_sensitive(true);
60 headers.insert("X-API-KEY", key_value);
61
62 let http = transport.build_client_with_headers(headers)?;
63 let base_url = Self::normalize_base_url(base_url, platform)?;
64
65 Ok(Self { http, base_url })
66 }
67
68 pub fn from_reqwest(
70 base_url: &str,
71 http: reqwest::Client,
72 platform: crate::ControllerPlatform,
73 ) -> Result<Self, Error> {
74 let base_url = Self::normalize_base_url(base_url, platform)?;
75 Ok(Self { http, base_url })
76 }
77
78 fn normalize_base_url(raw: &str, platform: crate::ControllerPlatform) -> Result<Url, Error> {
83 let mut url = Url::parse(raw)?;
84
85 let path = url.path().trim_end_matches('/').to_owned();
87
88 if path.ends_with("/integration") {
89 url.set_path(&format!("{path}/"));
90 } else {
91 let prefix = platform.integration_prefix();
92 url.set_path(&format!("{path}{prefix}/"));
93 }
94
95 Ok(url)
96 }
97
98 fn url(&self, path: &str) -> Url {
102 self.base_url
104 .join(path)
105 .expect("path should be valid relative URL")
106 }
107
108 async fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T, Error> {
111 let url = self.url(path);
112 debug!("GET {url}");
113
114 let resp = self.http.get(url).send().await?;
115 self.handle_response(resp).await
116 }
117
118 async fn get_with_params<T: DeserializeOwned>(
119 &self,
120 path: &str,
121 params: &[(&str, String)],
122 ) -> Result<T, Error> {
123 let url = self.url(path);
124 debug!("GET {url} params={params:?}");
125
126 let resp = self.http.get(url).query(params).send().await?;
127 self.handle_response(resp).await
128 }
129
130 async fn post<T: DeserializeOwned, B: Serialize + Sync>(
131 &self,
132 path: &str,
133 body: &B,
134 ) -> Result<T, Error> {
135 let url = self.url(path);
136 debug!("POST {url}");
137
138 let resp = self.http.post(url).json(body).send().await?;
139 self.handle_response(resp).await
140 }
141
142 async fn post_no_response<B: Serialize + Sync>(
143 &self,
144 path: &str,
145 body: &B,
146 ) -> Result<(), Error> {
147 let url = self.url(path);
148 debug!("POST {url}");
149
150 let resp = self.http.post(url).json(body).send().await?;
151 self.handle_empty(resp).await
152 }
153
154 async fn put<T: DeserializeOwned, B: Serialize + Sync>(
155 &self,
156 path: &str,
157 body: &B,
158 ) -> Result<T, Error> {
159 let url = self.url(path);
160 debug!("PUT {url}");
161
162 let resp = self.http.put(url).json(body).send().await?;
163 self.handle_response(resp).await
164 }
165
166 async fn patch<T: DeserializeOwned, B: Serialize + Sync>(
167 &self,
168 path: &str,
169 body: &B,
170 ) -> Result<T, Error> {
171 let url = self.url(path);
172 debug!("PATCH {url}");
173
174 let resp = self.http.patch(url).json(body).send().await?;
175 self.handle_response(resp).await
176 }
177
178 async fn delete(&self, path: &str) -> Result<(), Error> {
179 let url = self.url(path);
180 debug!("DELETE {url}");
181
182 let resp = self.http.delete(url).send().await?;
183 self.handle_empty(resp).await
184 }
185
186 async fn delete_with_response<T: DeserializeOwned>(&self, path: &str) -> Result<T, Error> {
187 let url = self.url(path);
188 debug!("DELETE {url}");
189
190 let resp = self.http.delete(url).send().await?;
191 self.handle_response(resp).await
192 }
193
194 async fn delete_with_params<T: DeserializeOwned>(
195 &self,
196 path: &str,
197 params: &[(&str, String)],
198 ) -> Result<T, Error> {
199 let url = self.url(path);
200 debug!("DELETE {url} params={params:?}");
201
202 let resp = self.http.delete(url).query(params).send().await?;
203 self.handle_response(resp).await
204 }
205
206 async fn handle_response<T: DeserializeOwned>(
209 &self,
210 resp: reqwest::Response,
211 ) -> Result<T, Error> {
212 let status = resp.status();
213 if status.is_success() {
214 let body = resp.text().await?;
215 serde_json::from_str(&body).map_err(|e| {
216 let preview = &body[..body.len().min(200)];
217 Error::Deserialization {
218 message: format!("{e} (body preview: {preview:?})"),
219 body,
220 }
221 })
222 } else {
223 Err(self.parse_error(status, resp).await)
224 }
225 }
226
227 async fn handle_empty(&self, resp: reqwest::Response) -> Result<(), Error> {
228 let status = resp.status();
229 if status.is_success() {
230 Ok(())
231 } else {
232 Err(self.parse_error(status, resp).await)
233 }
234 }
235
236 async fn parse_error(&self, status: reqwest::StatusCode, resp: reqwest::Response) -> Error {
237 if status == reqwest::StatusCode::UNAUTHORIZED {
238 return Error::InvalidApiKey;
239 }
240
241 let raw = resp.text().await.unwrap_or_default();
242
243 if let Ok(err) = serde_json::from_str::<ErrorResponse>(&raw) {
244 Error::Integration {
245 status: status.as_u16(),
246 message: err.message.unwrap_or_else(|| status.to_string()),
247 code: err.code,
248 }
249 } else {
250 Error::Integration {
251 status: status.as_u16(),
252 message: if raw.is_empty() {
253 status.to_string()
254 } else {
255 raw
256 },
257 code: None,
258 }
259 }
260 }
261
262 pub async fn paginate_all<T, F, Fut>(&self, limit: i32, fetch: F) -> Result<Vec<T>, Error>
266 where
267 F: Fn(i64, i32) -> Fut,
268 Fut: Future<Output = Result<types::Page<T>, Error>>,
269 {
270 let mut all = Vec::new();
271 let mut offset: i64 = 0;
272
273 loop {
274 let page = fetch(offset, limit).await?;
275 let received = page.data.len();
276 all.extend(page.data);
277
278 let limit_usize = usize::try_from(limit).unwrap_or(0);
279 if received < limit_usize
280 || i64::try_from(all.len()).unwrap_or(i64::MAX) >= page.total_count
281 {
282 break;
283 }
284
285 offset += i64::try_from(received).unwrap_or(i64::MAX);
286 }
287
288 Ok(all)
289 }
290
291 pub async fn get_info(&self) -> Result<types::ApplicationInfoResponse, Error> {
296 self.get("v1/info").await
297 }
298
299 pub async fn list_sites(
302 &self,
303 offset: i64,
304 limit: i32,
305 ) -> Result<types::Page<types::SiteResponse>, Error> {
306 self.get_with_params(
307 "v1/sites",
308 &[("offset", offset.to_string()), ("limit", limit.to_string())],
309 )
310 .await
311 }
312
313 pub async fn list_devices(
316 &self,
317 site_id: &Uuid,
318 offset: i64,
319 limit: i32,
320 ) -> Result<types::Page<types::DeviceResponse>, Error> {
321 self.get_with_params(
322 &format!("v1/sites/{site_id}/devices"),
323 &[("offset", offset.to_string()), ("limit", limit.to_string())],
324 )
325 .await
326 }
327
328 pub async fn get_device(
329 &self,
330 site_id: &Uuid,
331 device_id: &Uuid,
332 ) -> Result<types::DeviceDetailsResponse, Error> {
333 self.get(&format!("v1/sites/{site_id}/devices/{device_id}"))
334 .await
335 }
336
337 pub async fn get_device_statistics(
338 &self,
339 site_id: &Uuid,
340 device_id: &Uuid,
341 ) -> Result<types::DeviceStatisticsResponse, Error> {
342 self.get(&format!(
343 "v1/sites/{site_id}/devices/{device_id}/statistics/latest"
344 ))
345 .await
346 }
347
348 pub async fn adopt_device(
349 &self,
350 site_id: &Uuid,
351 mac: &str,
352 ignore_device_limit: bool,
353 ) -> Result<types::DeviceDetailsResponse, Error> {
354 #[derive(Serialize)]
355 #[serde(rename_all = "camelCase")]
356 struct Body<'a> {
357 mac_address: &'a str,
358 ignore_device_limit: bool,
359 }
360
361 self.post(
362 &format!("v1/sites/{site_id}/devices"),
363 &Body {
364 mac_address: mac,
365 ignore_device_limit,
366 },
367 )
368 .await
369 }
370
371 pub async fn remove_device(&self, site_id: &Uuid, device_id: &Uuid) -> Result<(), Error> {
372 self.delete(&format!("v1/sites/{site_id}/devices/{device_id}"))
373 .await
374 }
375
376 pub async fn device_action(
377 &self,
378 site_id: &Uuid,
379 device_id: &Uuid,
380 action: &str,
381 ) -> Result<(), Error> {
382 #[derive(Serialize)]
383 struct Body<'a> {
384 action: &'a str,
385 }
386
387 self.post_no_response(
388 &format!("v1/sites/{site_id}/devices/{device_id}/actions"),
389 &Body { action },
390 )
391 .await
392 }
393
394 pub async fn port_action(
395 &self,
396 site_id: &Uuid,
397 device_id: &Uuid,
398 port_idx: u32,
399 action: &str,
400 ) -> Result<(), Error> {
401 #[derive(Serialize)]
402 struct Body<'a> {
403 action: &'a str,
404 }
405
406 self.post_no_response(
407 &format!("v1/sites/{site_id}/devices/{device_id}/interfaces/ports/{port_idx}/actions"),
408 &Body { action },
409 )
410 .await
411 }
412
413 pub async fn list_pending_devices(
414 &self,
415 site_id: &Uuid,
416 offset: i64,
417 limit: i32,
418 ) -> Result<types::Page<types::PendingDeviceResponse>, Error> {
419 self.get_with_params(
420 &format!("v1/sites/{site_id}/devices/pending"),
421 &[("offset", offset.to_string()), ("limit", limit.to_string())],
422 )
423 .await
424 }
425
426 pub async fn list_device_tags(
427 &self,
428 site_id: &Uuid,
429 offset: i64,
430 limit: i32,
431 ) -> Result<types::Page<types::DeviceTagResponse>, Error> {
432 self.get_with_params(
433 &format!("v1/sites/{site_id}/devices/tags"),
434 &[("offset", offset.to_string()), ("limit", limit.to_string())],
435 )
436 .await
437 }
438
439 pub async fn list_clients(
442 &self,
443 site_id: &Uuid,
444 offset: i64,
445 limit: i32,
446 ) -> Result<types::Page<types::ClientResponse>, Error> {
447 self.get_with_params(
448 &format!("v1/sites/{site_id}/clients"),
449 &[("offset", offset.to_string()), ("limit", limit.to_string())],
450 )
451 .await
452 }
453
454 pub async fn get_client(
455 &self,
456 site_id: &Uuid,
457 client_id: &Uuid,
458 ) -> Result<types::ClientDetailsResponse, Error> {
459 self.get(&format!("v1/sites/{site_id}/clients/{client_id}"))
460 .await
461 }
462
463 pub async fn client_action(
464 &self,
465 site_id: &Uuid,
466 client_id: &Uuid,
467 action: &str,
468 ) -> Result<types::ClientActionResponse, Error> {
469 #[derive(Serialize)]
470 struct Body<'a> {
471 action: &'a str,
472 }
473
474 self.post(
475 &format!("v1/sites/{site_id}/clients/{client_id}/actions"),
476 &Body { action },
477 )
478 .await
479 }
480
481 pub async fn list_networks(
484 &self,
485 site_id: &Uuid,
486 offset: i64,
487 limit: i32,
488 ) -> Result<types::Page<types::NetworkResponse>, Error> {
489 self.get_with_params(
490 &format!("v1/sites/{site_id}/networks"),
491 &[("offset", offset.to_string()), ("limit", limit.to_string())],
492 )
493 .await
494 }
495
496 pub async fn get_network(
497 &self,
498 site_id: &Uuid,
499 network_id: &Uuid,
500 ) -> Result<types::NetworkDetailsResponse, Error> {
501 self.get(&format!("v1/sites/{site_id}/networks/{network_id}"))
502 .await
503 }
504
505 pub async fn create_network(
506 &self,
507 site_id: &Uuid,
508 body: &types::NetworkCreateUpdate,
509 ) -> Result<types::NetworkDetailsResponse, Error> {
510 self.post(&format!("v1/sites/{site_id}/networks"), body)
511 .await
512 }
513
514 pub async fn update_network(
515 &self,
516 site_id: &Uuid,
517 network_id: &Uuid,
518 body: &types::NetworkCreateUpdate,
519 ) -> Result<types::NetworkDetailsResponse, Error> {
520 self.put(&format!("v1/sites/{site_id}/networks/{network_id}"), body)
521 .await
522 }
523
524 pub async fn delete_network(&self, site_id: &Uuid, network_id: &Uuid) -> Result<(), Error> {
525 self.delete(&format!("v1/sites/{site_id}/networks/{network_id}"))
526 .await
527 }
528
529 pub async fn get_network_references(
530 &self,
531 site_id: &Uuid,
532 network_id: &Uuid,
533 ) -> Result<types::NetworkReferencesResponse, Error> {
534 self.get(&format!(
535 "v1/sites/{site_id}/networks/{network_id}/references"
536 ))
537 .await
538 }
539
540 pub async fn list_wifi_broadcasts(
543 &self,
544 site_id: &Uuid,
545 offset: i64,
546 limit: i32,
547 ) -> Result<types::Page<types::WifiBroadcastResponse>, Error> {
548 self.get_with_params(
549 &format!("v1/sites/{site_id}/wifi/broadcasts"),
550 &[("offset", offset.to_string()), ("limit", limit.to_string())],
551 )
552 .await
553 }
554
555 pub async fn get_wifi_broadcast(
556 &self,
557 site_id: &Uuid,
558 broadcast_id: &Uuid,
559 ) -> Result<types::WifiBroadcastDetailsResponse, Error> {
560 self.get(&format!(
561 "v1/sites/{site_id}/wifi/broadcasts/{broadcast_id}"
562 ))
563 .await
564 }
565
566 pub async fn create_wifi_broadcast(
567 &self,
568 site_id: &Uuid,
569 body: &types::WifiBroadcastCreateUpdate,
570 ) -> Result<types::WifiBroadcastDetailsResponse, Error> {
571 self.post(&format!("v1/sites/{site_id}/wifi/broadcasts"), body)
572 .await
573 }
574
575 pub async fn update_wifi_broadcast(
576 &self,
577 site_id: &Uuid,
578 broadcast_id: &Uuid,
579 body: &types::WifiBroadcastCreateUpdate,
580 ) -> Result<types::WifiBroadcastDetailsResponse, Error> {
581 self.put(
582 &format!("v1/sites/{site_id}/wifi/broadcasts/{broadcast_id}"),
583 body,
584 )
585 .await
586 }
587
588 pub async fn delete_wifi_broadcast(
589 &self,
590 site_id: &Uuid,
591 broadcast_id: &Uuid,
592 ) -> Result<(), Error> {
593 self.delete(&format!(
594 "v1/sites/{site_id}/wifi/broadcasts/{broadcast_id}"
595 ))
596 .await
597 }
598
599 pub async fn list_firewall_policies(
602 &self,
603 site_id: &Uuid,
604 offset: i64,
605 limit: i32,
606 ) -> Result<types::Page<types::FirewallPolicyResponse>, Error> {
607 self.get_with_params(
608 &format!("v1/sites/{site_id}/firewall/policies"),
609 &[("offset", offset.to_string()), ("limit", limit.to_string())],
610 )
611 .await
612 }
613
614 pub async fn get_firewall_policy(
615 &self,
616 site_id: &Uuid,
617 policy_id: &Uuid,
618 ) -> Result<types::FirewallPolicyResponse, Error> {
619 self.get(&format!("v1/sites/{site_id}/firewall/policies/{policy_id}"))
620 .await
621 }
622
623 pub async fn create_firewall_policy(
624 &self,
625 site_id: &Uuid,
626 body: &types::FirewallPolicyCreateUpdate,
627 ) -> Result<types::FirewallPolicyResponse, Error> {
628 self.post(&format!("v1/sites/{site_id}/firewall/policies"), body)
629 .await
630 }
631
632 pub async fn update_firewall_policy(
633 &self,
634 site_id: &Uuid,
635 policy_id: &Uuid,
636 body: &types::FirewallPolicyCreateUpdate,
637 ) -> Result<types::FirewallPolicyResponse, Error> {
638 self.put(
639 &format!("v1/sites/{site_id}/firewall/policies/{policy_id}"),
640 body,
641 )
642 .await
643 }
644
645 pub async fn patch_firewall_policy(
646 &self,
647 site_id: &Uuid,
648 policy_id: &Uuid,
649 body: &types::FirewallPolicyPatch,
650 ) -> Result<types::FirewallPolicyResponse, Error> {
651 self.patch(
652 &format!("v1/sites/{site_id}/firewall/policies/{policy_id}"),
653 body,
654 )
655 .await
656 }
657
658 pub async fn delete_firewall_policy(
659 &self,
660 site_id: &Uuid,
661 policy_id: &Uuid,
662 ) -> Result<(), Error> {
663 self.delete(&format!("v1/sites/{site_id}/firewall/policies/{policy_id}"))
664 .await
665 }
666
667 pub async fn get_firewall_policy_ordering(
668 &self,
669 site_id: &Uuid,
670 ) -> Result<types::FirewallPolicyOrdering, Error> {
671 self.get(&format!("v1/sites/{site_id}/firewall/policies/ordering"))
672 .await
673 }
674
675 pub async fn set_firewall_policy_ordering(
676 &self,
677 site_id: &Uuid,
678 body: &types::FirewallPolicyOrdering,
679 ) -> Result<types::FirewallPolicyOrdering, Error> {
680 self.put(
681 &format!("v1/sites/{site_id}/firewall/policies/ordering"),
682 body,
683 )
684 .await
685 }
686
687 pub async fn list_firewall_zones(
690 &self,
691 site_id: &Uuid,
692 offset: i64,
693 limit: i32,
694 ) -> Result<types::Page<types::FirewallZoneResponse>, Error> {
695 self.get_with_params(
696 &format!("v1/sites/{site_id}/firewall/zones"),
697 &[("offset", offset.to_string()), ("limit", limit.to_string())],
698 )
699 .await
700 }
701
702 pub async fn get_firewall_zone(
703 &self,
704 site_id: &Uuid,
705 zone_id: &Uuid,
706 ) -> Result<types::FirewallZoneResponse, Error> {
707 self.get(&format!("v1/sites/{site_id}/firewall/zones/{zone_id}"))
708 .await
709 }
710
711 pub async fn create_firewall_zone(
712 &self,
713 site_id: &Uuid,
714 body: &types::FirewallZoneCreateUpdate,
715 ) -> Result<types::FirewallZoneResponse, Error> {
716 self.post(&format!("v1/sites/{site_id}/firewall/zones"), body)
717 .await
718 }
719
720 pub async fn update_firewall_zone(
721 &self,
722 site_id: &Uuid,
723 zone_id: &Uuid,
724 body: &types::FirewallZoneCreateUpdate,
725 ) -> Result<types::FirewallZoneResponse, Error> {
726 self.put(
727 &format!("v1/sites/{site_id}/firewall/zones/{zone_id}"),
728 body,
729 )
730 .await
731 }
732
733 pub async fn delete_firewall_zone(&self, site_id: &Uuid, zone_id: &Uuid) -> Result<(), Error> {
734 self.delete(&format!("v1/sites/{site_id}/firewall/zones/{zone_id}"))
735 .await
736 }
737
738 pub async fn list_acl_rules(
741 &self,
742 site_id: &Uuid,
743 offset: i64,
744 limit: i32,
745 ) -> Result<types::Page<types::AclRuleResponse>, Error> {
746 self.get_with_params(
747 &format!("v1/sites/{site_id}/acl-rules"),
748 &[("offset", offset.to_string()), ("limit", limit.to_string())],
749 )
750 .await
751 }
752
753 pub async fn get_acl_rule(
754 &self,
755 site_id: &Uuid,
756 rule_id: &Uuid,
757 ) -> Result<types::AclRuleResponse, Error> {
758 self.get(&format!("v1/sites/{site_id}/acl-rules/{rule_id}"))
759 .await
760 }
761
762 pub async fn create_acl_rule(
763 &self,
764 site_id: &Uuid,
765 body: &types::AclRuleCreateUpdate,
766 ) -> Result<types::AclRuleResponse, Error> {
767 self.post(&format!("v1/sites/{site_id}/acl-rules"), body)
768 .await
769 }
770
771 pub async fn update_acl_rule(
772 &self,
773 site_id: &Uuid,
774 rule_id: &Uuid,
775 body: &types::AclRuleCreateUpdate,
776 ) -> Result<types::AclRuleResponse, Error> {
777 self.put(&format!("v1/sites/{site_id}/acl-rules/{rule_id}"), body)
778 .await
779 }
780
781 pub async fn delete_acl_rule(&self, site_id: &Uuid, rule_id: &Uuid) -> Result<(), Error> {
782 self.delete(&format!("v1/sites/{site_id}/acl-rules/{rule_id}"))
783 .await
784 }
785
786 pub async fn get_acl_rule_ordering(
787 &self,
788 site_id: &Uuid,
789 ) -> Result<types::AclRuleOrdering, Error> {
790 self.get(&format!("v1/sites/{site_id}/acl-rules/ordering"))
791 .await
792 }
793
794 pub async fn set_acl_rule_ordering(
795 &self,
796 site_id: &Uuid,
797 body: &types::AclRuleOrdering,
798 ) -> Result<types::AclRuleOrdering, Error> {
799 self.put(&format!("v1/sites/{site_id}/acl-rules/ordering"), body)
800 .await
801 }
802
803 pub async fn list_dns_policies(
806 &self,
807 site_id: &Uuid,
808 offset: i64,
809 limit: i32,
810 ) -> Result<types::Page<types::DnsPolicyResponse>, Error> {
811 self.get_with_params(
812 &format!("v1/sites/{site_id}/dns/policies"),
813 &[("offset", offset.to_string()), ("limit", limit.to_string())],
814 )
815 .await
816 }
817
818 pub async fn get_dns_policy(
819 &self,
820 site_id: &Uuid,
821 dns_id: &Uuid,
822 ) -> Result<types::DnsPolicyResponse, Error> {
823 self.get(&format!("v1/sites/{site_id}/dns/policies/{dns_id}"))
824 .await
825 }
826
827 pub async fn create_dns_policy(
828 &self,
829 site_id: &Uuid,
830 body: &types::DnsPolicyCreateUpdate,
831 ) -> Result<types::DnsPolicyResponse, Error> {
832 self.post(&format!("v1/sites/{site_id}/dns/policies"), body)
833 .await
834 }
835
836 pub async fn update_dns_policy(
837 &self,
838 site_id: &Uuid,
839 dns_id: &Uuid,
840 body: &types::DnsPolicyCreateUpdate,
841 ) -> Result<types::DnsPolicyResponse, Error> {
842 self.put(&format!("v1/sites/{site_id}/dns/policies/{dns_id}"), body)
843 .await
844 }
845
846 pub async fn delete_dns_policy(&self, site_id: &Uuid, dns_id: &Uuid) -> Result<(), Error> {
847 self.delete(&format!("v1/sites/{site_id}/dns/policies/{dns_id}"))
848 .await
849 }
850
851 pub async fn list_traffic_matching_lists(
854 &self,
855 site_id: &Uuid,
856 offset: i64,
857 limit: i32,
858 ) -> Result<types::Page<types::TrafficMatchingListResponse>, Error> {
859 self.get_with_params(
860 &format!("v1/sites/{site_id}/traffic-matching-lists"),
861 &[("offset", offset.to_string()), ("limit", limit.to_string())],
862 )
863 .await
864 }
865
866 pub async fn get_traffic_matching_list(
867 &self,
868 site_id: &Uuid,
869 list_id: &Uuid,
870 ) -> Result<types::TrafficMatchingListResponse, Error> {
871 self.get(&format!(
872 "v1/sites/{site_id}/traffic-matching-lists/{list_id}"
873 ))
874 .await
875 }
876
877 pub async fn create_traffic_matching_list(
878 &self,
879 site_id: &Uuid,
880 body: &types::TrafficMatchingListCreateUpdate,
881 ) -> Result<types::TrafficMatchingListResponse, Error> {
882 self.post(&format!("v1/sites/{site_id}/traffic-matching-lists"), body)
883 .await
884 }
885
886 pub async fn update_traffic_matching_list(
887 &self,
888 site_id: &Uuid,
889 list_id: &Uuid,
890 body: &types::TrafficMatchingListCreateUpdate,
891 ) -> Result<types::TrafficMatchingListResponse, Error> {
892 self.put(
893 &format!("v1/sites/{site_id}/traffic-matching-lists/{list_id}"),
894 body,
895 )
896 .await
897 }
898
899 pub async fn delete_traffic_matching_list(
900 &self,
901 site_id: &Uuid,
902 list_id: &Uuid,
903 ) -> Result<(), Error> {
904 self.delete(&format!(
905 "v1/sites/{site_id}/traffic-matching-lists/{list_id}"
906 ))
907 .await
908 }
909
910 pub async fn list_vouchers(
913 &self,
914 site_id: &Uuid,
915 offset: i64,
916 limit: i32,
917 ) -> Result<types::Page<types::VoucherResponse>, Error> {
918 self.get_with_params(
919 &format!("v1/sites/{site_id}/hotspot/vouchers"),
920 &[("offset", offset.to_string()), ("limit", limit.to_string())],
921 )
922 .await
923 }
924
925 pub async fn get_voucher(
926 &self,
927 site_id: &Uuid,
928 voucher_id: &Uuid,
929 ) -> Result<types::VoucherResponse, Error> {
930 self.get(&format!("v1/sites/{site_id}/hotspot/vouchers/{voucher_id}"))
931 .await
932 }
933
934 pub async fn create_vouchers(
935 &self,
936 site_id: &Uuid,
937 body: &types::VoucherCreateRequest,
938 ) -> Result<Vec<types::VoucherResponse>, Error> {
939 self.post(&format!("v1/sites/{site_id}/hotspot/vouchers"), body)
940 .await
941 }
942
943 pub async fn delete_voucher(
944 &self,
945 site_id: &Uuid,
946 voucher_id: &Uuid,
947 ) -> Result<types::VoucherDeletionResults, Error> {
948 self.delete_with_response(&format!("v1/sites/{site_id}/hotspot/vouchers/{voucher_id}"))
949 .await
950 }
951
952 pub async fn purge_vouchers(
953 &self,
954 site_id: &Uuid,
955 filter: &str,
956 ) -> Result<types::VoucherDeletionResults, Error> {
957 self.delete_with_params(
958 &format!("v1/sites/{site_id}/hotspot/vouchers"),
959 &[("filter", filter.to_owned())],
960 )
961 .await
962 }
963
964 pub async fn list_vpn_servers(
967 &self,
968 site_id: &Uuid,
969 offset: i64,
970 limit: i32,
971 ) -> Result<types::Page<types::VpnServerResponse>, Error> {
972 self.get_with_params(
973 &format!("v1/sites/{site_id}/vpn/servers"),
974 &[("offset", offset.to_string()), ("limit", limit.to_string())],
975 )
976 .await
977 }
978
979 pub async fn list_vpn_tunnels(
980 &self,
981 site_id: &Uuid,
982 offset: i64,
983 limit: i32,
984 ) -> Result<types::Page<types::VpnTunnelResponse>, Error> {
985 self.get_with_params(
986 &format!("v1/sites/{site_id}/vpn/tunnels"),
987 &[("offset", offset.to_string()), ("limit", limit.to_string())],
988 )
989 .await
990 }
991
992 pub async fn list_wans(
995 &self,
996 site_id: &Uuid,
997 offset: i64,
998 limit: i32,
999 ) -> Result<types::Page<types::WanResponse>, Error> {
1000 self.get_with_params(
1001 &format!("v1/sites/{site_id}/wans"),
1002 &[("offset", offset.to_string()), ("limit", limit.to_string())],
1003 )
1004 .await
1005 }
1006
1007 pub async fn list_dpi_categories(
1010 &self,
1011 site_id: &Uuid,
1012 offset: i64,
1013 limit: i32,
1014 ) -> Result<types::Page<types::DpiCategoryResponse>, Error> {
1015 self.get_with_params(
1016 &format!("v1/sites/{site_id}/dpi/categories"),
1017 &[("offset", offset.to_string()), ("limit", limit.to_string())],
1018 )
1019 .await
1020 }
1021
1022 pub async fn list_dpi_applications(
1023 &self,
1024 site_id: &Uuid,
1025 offset: i64,
1026 limit: i32,
1027 ) -> Result<types::Page<types::DpiApplicationResponse>, Error> {
1028 self.get_with_params(
1029 &format!("v1/sites/{site_id}/dpi/applications"),
1030 &[("offset", offset.to_string()), ("limit", limit.to_string())],
1031 )
1032 .await
1033 }
1034
1035 pub async fn list_radius_profiles(
1038 &self,
1039 site_id: &Uuid,
1040 offset: i64,
1041 limit: i32,
1042 ) -> Result<types::Page<types::RadiusProfileResponse>, Error> {
1043 self.get_with_params(
1044 &format!("v1/sites/{site_id}/radius/profiles"),
1045 &[("offset", offset.to_string()), ("limit", limit.to_string())],
1046 )
1047 .await
1048 }
1049
1050 pub async fn list_countries(
1053 &self,
1054 offset: i64,
1055 limit: i32,
1056 ) -> Result<types::Page<types::CountryResponse>, Error> {
1057 self.get_with_params(
1058 "v1/countries",
1059 &[("offset", offset.to_string()), ("limit", limit.to_string())],
1060 )
1061 .await
1062 }
1063}