bing_webmaster_api/
client.rs

1use crate::dto::*;
2use crate::error::{map_status_error, try_parse_api_error, Result, WebmasterApiError};
3use reqwest::{Client, Response};
4use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
5use serde_json::json;
6use tracing::instrument;
7
8/// Bing Webmaster API client for interacting with Bing Webmaster Tools
9///
10/// This client provides access to all methods from the Bing Webmaster API,
11/// allowing you to manage your websites, submit URLs, track crawl issues, and more.
12#[derive(Debug, Clone)]
13pub struct BingWebmasterClient {
14    client: ClientWithMiddleware,
15    base_url: String,
16    api_key: String,
17}
18
19impl BingWebmasterClient {
20    /// Helper method to handle API responses with proper error handling
21    async fn handle_response<T: for<'de> serde::Deserialize<'de>>(
22        &self,
23        response: Response,
24    ) -> Result<T> {
25        let status = response.status();
26
27        if status.is_success() {
28            let response_text = response.text().await?;
29
30            println!("{}", &response_text);
31
32            // Try to deserialize as wrapped response
33            match serde_json::from_str::<ResponseWrapper<T>>(&response_text) {
34                Ok(wrapper) => Ok(wrapper.d),
35                Err(json_err) => {
36                    // Try to deserialize directly
37                    match serde_json::from_str::<T>(&response_text) {
38                        Ok(data) => Ok(data),
39                        Err(_) => {
40                            // Check if response contains an API error
41                            if let Some((error_code, message)) = try_parse_api_error(&response_text)
42                            {
43                                Err(WebmasterApiError::api_error(
44                                    error_code,
45                                    message,
46                                    Some(status.as_u16()),
47                                ))
48                            } else {
49                                Err(WebmasterApiError::invalid_response(format!(
50                                    "Failed to parse response: {}",
51                                    json_err
52                                )))
53                            }
54                        }
55                    }
56                }
57            }
58        } else {
59            let response_text = response.text().await.unwrap_or_default();
60
61            // Check if response contains an API error
62            if let Some((error_code, message)) = try_parse_api_error(&response_text) {
63                Err(WebmasterApiError::api_error(
64                    error_code,
65                    message,
66                    Some(status.as_u16()),
67                ))
68            } else {
69                Err(map_status_error(status, response_text))
70            }
71        }
72    }
73
74    /// Helper method to handle void responses (methods that return void in .NET)
75    async fn handle_void_response(&self, response: Response) -> Result<()> {
76        let status = response.status();
77
78        if status.is_success() {
79            Ok(())
80        } else {
81            let response_text = response.text().await.unwrap_or_default();
82
83            // Check if response contains an API error
84            if let Some((error_code, message)) = try_parse_api_error(&response_text) {
85                Err(WebmasterApiError::api_error(
86                    error_code,
87                    message,
88                    Some(status.as_u16()),
89                ))
90            } else {
91                Err(map_status_error(status, response_text))
92            }
93        }
94    }
95
96    pub fn new(api_key: String) -> Self {
97        let client = ClientBuilder::new(Client::new()).build();
98        Self {
99            client,
100            base_url: "https://ssl.bing.com/webmaster/api.svc".to_string(),
101            api_key,
102        }
103    }
104
105    /// Create a new WebmasterApiClient with default reqwest client
106    ///
107    /// # Arguments
108    /// * `api_key` - Your Bing Webmaster Tools API key
109    /// * `base_url` - Optional custom base URL (defaults to official Bing API endpoint)
110    pub fn with_base_url(api_key: String, base_url: String) -> Self {
111        let client = ClientBuilder::new(Client::new()).build();
112        Self {
113            client,
114            base_url,
115            api_key,
116        }
117    }
118
119    /// Create a new WebmasterApiClient with custom middleware
120    ///
121    /// # Arguments
122    /// * `api_key` - Your Bing Webmaster Tools API key
123    /// * `base_url` - Optional custom base URL (defaults to official Bing API endpoint)
124    /// * `client_builder` - ClientBuilder with configured middleware
125    ///
126    /// # Examples
127    ///
128    /// ```rust,no_run
129    /// use bing_webmaster_api::BingWebmasterClient;
130    /// use reqwest::Client;
131    /// use reqwest_middleware::ClientBuilder;
132    ///
133    /// let client_builder = ClientBuilder::new(Client::new());
134    /// let api_client = BingWebmasterClient::with_middleware(
135    ///     "your-api-key".to_string(),
136    ///     None,
137    ///     client_builder
138    /// );
139    /// ```
140    pub fn with_middleware(
141        api_key: String,
142        base_url: Option<String>,
143        client_builder: ClientBuilder,
144    ) -> Self {
145        Self {
146            client: client_builder.build(),
147            base_url: base_url
148                .unwrap_or_else(|| "https://ssl.bing.com/webmaster/api.svc".to_string()),
149            api_key,
150        }
151    }
152
153    // Write methods (return void in .NET API)
154
155    #[instrument(skip(self))]
156    pub async fn add_blocked_url(&self, site_url: &str, blocked_url: &BlockedUrl) -> Result<()> {
157        let url = format!(
158            "{}/json/AddBlockedUrl?apikey={}",
159            self.base_url, self.api_key
160        );
161        let body = json!({
162            "siteUrl": site_url,
163            "blockedUrl": blocked_url
164        });
165
166        let response = self
167            .client
168            .post(&url)
169            .header("Content-Type", "application/json; charset=utf-8")
170            .body(serde_json::to_string(&body)?)
171            .send()
172            .await?;
173
174        self.handle_void_response(response).await
175    }
176
177    #[instrument(skip(self))]
178    pub async fn add_connected_page(&self, site_url: &str, master_url: &str) -> Result<()> {
179        let url = format!(
180            "{}/json/AddConnectedPage?apikey={}",
181            self.base_url, self.api_key
182        );
183        let body = json!({
184            "siteUrl": site_url,
185            "masterUrl": master_url
186        });
187
188        let response = self
189            .client
190            .post(&url)
191            .header("Content-Type", "application/json; charset=utf-8")
192            .body(serde_json::to_string(&body)?)
193            .send()
194            .await?;
195
196        self.handle_void_response(response).await
197    }
198
199    #[instrument(skip(self))]
200    pub async fn add_country_region_settings(
201        &self,
202        site_url: &str,
203        settings: &CountryRegionSettings,
204    ) -> Result<()> {
205        let url = format!(
206            "{}/json/AddCountryRegionSettings?apikey={}",
207            self.base_url, self.api_key
208        );
209        let body = json!({
210            "siteUrl": site_url,
211            "settings": settings
212        });
213
214        let response = self
215            .client
216            .post(&url)
217            .header("Content-Type", "application/json; charset=utf-8")
218            .body(serde_json::to_string(&body)?)
219            .send()
220            .await?;
221
222        self.handle_void_response(response).await
223    }
224
225    #[instrument(skip(self))]
226    pub async fn add_deep_link_block(
227        &self,
228        site_url: &str,
229        market: &str,
230        search_url: &str,
231        deep_link_url: &str,
232    ) -> Result<()> {
233        let url = format!(
234            "{}/json/AddDeepLinkBlock?apikey={}",
235            self.base_url, self.api_key
236        );
237        let body = json!({
238            "siteUrl": site_url,
239            "market": market,
240            "searchUrl": search_url,
241            "deepLinkUrl": deep_link_url
242        });
243
244        let response = self
245            .client
246            .post(&url)
247            .header("Content-Type", "application/json; charset=utf-8")
248            .body(serde_json::to_string(&body)?)
249            .send()
250            .await?;
251
252        self.handle_void_response(response).await
253    }
254
255    #[instrument(skip(self))]
256    pub async fn add_query_parameter(&self, site_url: &str, query_parameter: &str) -> Result<()> {
257        let url = format!(
258            "{}/json/AddQueryParameter?apikey={}",
259            self.base_url, self.api_key
260        );
261        let body = json!({
262            "siteUrl": site_url,
263            "queryParameter": query_parameter
264        });
265
266        let response = self
267            .client
268            .post(&url)
269            .header("Content-Type", "application/json; charset=utf-8")
270            .body(serde_json::to_string(&body)?)
271            .send()
272            .await?;
273
274        self.handle_void_response(response).await
275    }
276
277    #[instrument(skip(self))]
278    pub async fn add_site(&self, site_url: &str) -> Result<()> {
279        let url = format!("{}/json/AddSite?apikey={}", self.base_url, self.api_key);
280        let body = json!({
281            "siteUrl": site_url
282        });
283
284        let response = self
285            .client
286            .post(&url)
287            .header("Content-Type", "application/json; charset=utf-8")
288            .body(serde_json::to_string(&body)?)
289            .send()
290            .await?;
291
292        self.handle_void_response(response).await
293    }
294
295    #[instrument(skip(self))]
296    pub async fn add_site_roles(
297        &self,
298        site_url: &str,
299        delegated_url: &str,
300        user_email: &str,
301        authentication_code: &str,
302        is_administrator: bool,
303        is_read_only: bool,
304    ) -> Result<()> {
305        let url = format!(
306            "{}/json/AddSiteRoles?apikey={}",
307            self.base_url, self.api_key
308        );
309        let body = json!({
310            "siteUrl": site_url,
311            "delegatedUrl": delegated_url,
312            "userEmail": user_email,
313            "authenticationCode": authentication_code,
314            "isAdministrator": is_administrator,
315            "isReadOnly": is_read_only
316        });
317
318        let response = self
319            .client
320            .post(&url)
321            .header("Content-Type", "application/json; charset=utf-8")
322            .body(serde_json::to_string(&body)?)
323            .send()
324            .await?;
325
326        self.handle_void_response(response).await
327    }
328
329    #[instrument(skip(self))]
330    pub async fn enable_disable_query_parameter(
331        &self,
332        site_url: &str,
333        parameter: &str,
334        enabled: bool,
335    ) -> Result<()> {
336        let url = format!(
337            "{}/json/EnableDisableQueryParameter?apikey={}",
338            self.base_url, self.api_key
339        );
340        let body = json!({
341            "siteUrl": site_url,
342            "queryParameter": parameter,
343            "isEnabled": enabled
344        });
345
346        let response = self
347            .client
348            .post(&url)
349            .header("Content-Type", "application/json; charset=utf-8")
350            .body(serde_json::to_string(&body)?)
351            .send()
352            .await?;
353
354        self.handle_void_response(response).await
355    }
356
357    #[instrument(skip(self))]
358    pub async fn fetch_url(&self, site_url: &str, url: &str) -> Result<()> {
359        let api_url = format!("{}/json/FetchUrl?apikey={}", self.base_url, self.api_key);
360        let body = json!({
361            "siteUrl": site_url,
362            "url": url
363        });
364
365        let response = self
366            .client
367            .post(&api_url)
368            .header("Content-Type", "application/json; charset=utf-8")
369            .body(serde_json::to_string(&body)?)
370            .send()
371            .await?;
372
373        self.handle_void_response(response).await
374    }
375
376    /// Submit content
377    ///
378    /// ### Parameters
379    /// * siteUrl - Site url E.g.: http://example.com
380    /// * url - Url to submit E.g.: http://example.com/url1.html
381    /// * httpMessage - HTTP status line, such as HTTP/1.1 200 OK, HTTP Headers, an empty line and optional HTTP message body data equivalent of the response based64 encoded if bingbot was fetching this URL. The request/status line and headers must all end with <CR> <LF> (that is, a carriage return followed by a line feed). The empty line must consist of only <CR> <LF> and no other whitespace.
382    /// * structuredData - Structured Data (typically JSON-LD ) provided based64 encoded typically used for submitting to bing structured Data for non-HTML content types as images, PDF files.Empty if no structured data provided.
383    /// * dynamicServing - {none = 0, PC-laptop = 1, mobile = 2, AMP = 3, tablet = 4, non-visual browser = 5}. Set this field to a value greater than 0 only if your web site dynamically serves different content based on customer devices visiting your web site
384    #[instrument(skip(self))]
385    pub async fn submit_content(
386        &self,
387        site_url: &str,
388        url: &str,
389        http_message: &str,
390        structured_data: &str,
391        dynamic_serving: i32,
392    ) -> Result<()> {
393        let api_url = format!(
394            "{}/json/SubmitContent?apikey={}",
395            self.base_url, self.api_key
396        );
397        let body = json!({
398            "siteUrl": site_url,
399            "url": url,
400            "httpMessage": http_message,
401            "structuredData": structured_data,
402            "dynamicServing": dynamic_serving
403        });
404
405        let response = self
406            .client
407            .post(&api_url)
408            .header("Content-Type", "application/json; charset=utf-8")
409            .body(serde_json::to_string(&body)?)
410            .send()
411            .await?;
412
413        self.handle_void_response(response).await
414    }
415
416    #[instrument(skip(self))]
417    pub async fn submit_url(&self, site_url: &str, url: &str) -> Result<()> {
418        let api_url = format!("{}/json/SubmitUrl?apikey={}", self.base_url, self.api_key);
419        let body = json!({
420            "siteUrl": site_url,
421            "url": url
422        });
423
424        let response = self
425            .client
426            .post(&api_url)
427            .header("Content-Type", "application/json; charset=utf-8")
428            .body(serde_json::to_string(&body)?)
429            .send()
430            .await?;
431
432        self.handle_void_response(response).await
433    }
434
435    #[instrument(skip(self))]
436    pub async fn submit_url_batch(&self, site_url: &str, url_list: &[String]) -> Result<()> {
437        let api_url = format!(
438            "{}/json/SubmitUrlBatch?apikey={}",
439            self.base_url, self.api_key
440        );
441        let body = json!({
442            "siteUrl": site_url,
443            "urlList": url_list
444        });
445
446        let response = self
447            .client
448            .post(&api_url)
449            .header("Content-Type", "application/json; charset=utf-8")
450            .body(serde_json::to_string(&body)?)
451            .send()
452            .await?;
453
454        self.handle_void_response(response).await
455    }
456
457    // Read methods (return data)
458
459    #[instrument(skip(self))]
460    pub async fn get_crawl_issues(&self, site_url: &str) -> Result<Vec<UrlWithCrawlIssues>> {
461        let url = format!(
462            "{}/json/GetCrawlIssues?apikey={}&siteUrl={}",
463            self.base_url,
464            self.api_key,
465            urlencoding::encode(site_url)
466        );
467
468        let response = self.client.get(&url).send().await?;
469
470        self.handle_response(response).await
471    }
472
473    #[instrument(skip(self))]
474    pub async fn get_page_stats(&self, site_url: &str) -> Result<Vec<QueryStats>> {
475        let url = format!(
476            "{}/json/GetPageStats?apikey={}&siteUrl={}",
477            self.base_url,
478            self.api_key,
479            urlencoding::encode(site_url)
480        );
481
482        let response = self.client.get(&url).send().await?;
483
484        self.handle_response(response).await
485    }
486
487    #[instrument(skip(self))]
488    pub async fn get_connected_pages(&self, site_url: &str) -> Result<Vec<Site>> {
489        let url = format!(
490            "{}/json/GetConnectedPages?apikey={}&siteUrl={}",
491            self.base_url,
492            self.api_key,
493            urlencoding::encode(site_url)
494        );
495
496        let response = self.client.get(&url).send().await?;
497
498        self.handle_response(response).await
499    }
500
501    #[instrument(skip(self))]
502    pub async fn get_query_parameters(&self, site_url: &str) -> Result<Vec<QueryParameter>> {
503        let url = format!(
504            "{}/json/GetQueryParameters?apikey={}&siteUrl={}",
505            self.base_url,
506            self.api_key,
507            urlencoding::encode(site_url)
508        );
509
510        let response = self.client.get(&url).send().await?;
511
512        self.handle_response(response).await
513    }
514
515    #[instrument(skip(self))]
516    pub async fn get_crawl_stats(&self, site_url: &str) -> Result<Vec<CrawlStats>> {
517        let url = format!(
518            "{}/json/GetCrawlStats?apikey={}&siteUrl={}",
519            self.base_url,
520            self.api_key,
521            urlencoding::encode(site_url)
522        );
523
524        let response = self.client.get(&url).send().await?;
525
526        self.handle_response(response).await
527    }
528
529    #[instrument(skip(self))]
530    pub async fn verify_site(&self, site_url: &str) -> Result<bool> {
531        let url = format!("{}/json/VerifySite?apikey={}", self.base_url, self.api_key);
532        let body = json!({
533            "siteUrl": site_url
534        });
535
536        let response = self
537            .client
538            .post(&url)
539            .header("Content-Type", "application/json; charset=utf-8")
540            .body(serde_json::to_string(&body)?)
541            .send()
542            .await?;
543
544        self.handle_response(response).await
545    }
546
547    #[instrument(skip(self))]
548    pub async fn get_content_submission_quota(
549        &self,
550        site_url: &str,
551    ) -> Result<ContentSubmissionQuota> {
552        let url = format!(
553            "{}/json/GetContentSubmissionQuota?apikey={}&siteUrl={}",
554            self.base_url,
555            self.api_key,
556            urlencoding::encode(site_url)
557        );
558
559        let response = self.client.get(&url).send().await?;
560
561        self.handle_response(response).await
562    }
563
564    #[instrument(skip(self))]
565    pub async fn get_blocked_urls(&self, site_url: &str) -> Result<Vec<BlockedUrl>> {
566        let url = format!(
567            "{}/json/GetBlockedUrls?apikey={}&siteUrl={}",
568            self.base_url,
569            self.api_key,
570            urlencoding::encode(site_url)
571        );
572
573        let response = self.client.get(&url).send().await?;
574
575        self.handle_response(response).await
576    }
577
578    #[instrument(skip(self))]
579    pub async fn get_children_url_info(
580        &self,
581        site_url: &str,
582        url: &str,
583        page: u16,
584        filter_properties: &FilterProperties,
585    ) -> Result<Vec<UrlInfo>> {
586        let api_url = format!(
587            "{}/json/GetChildrenUrlInfo?apikey={}",
588            self.base_url, self.api_key
589        );
590
591        let body = serde_json::json!({
592            "siteUrl": site_url,
593            "url": url,
594            "page": page,
595            "filterProperties": filter_properties
596        });
597
598        let response = self
599            .client
600            .post(&api_url)
601            .header("Content-Type", "application/json; charset=utf-8")
602            .body(serde_json::to_string(&body)?)
603            .send()
604            .await?;
605
606        self.handle_response(response).await
607    }
608
609    #[instrument(skip(self))]
610    pub async fn get_children_url_traffic_info(
611        &self,
612        site_url: &str,
613        url: &str,
614        page: u16,
615    ) -> Result<Vec<UrlTrafficInfo>> {
616        let api_url = format!(
617            "{}/json/GetChildrenUrlTrafficInfo?apikey={}&siteUrl={}&url={}&page={}",
618            self.base_url,
619            self.api_key,
620            urlencoding::encode(site_url),
621            urlencoding::encode(url),
622            page
623        );
624
625        let response = self.client.get(&api_url).send().await?;
626
627        self.handle_response(response).await
628    }
629
630    #[instrument(skip(self))]
631    pub async fn get_country_region_settings(
632        &self,
633        site_url: &str,
634    ) -> Result<Vec<CountryRegionSettings>> {
635        let url = format!(
636            "{}/json/GetCountryRegionSettings?apikey={}&siteUrl={}",
637            self.base_url,
638            self.api_key,
639            urlencoding::encode(site_url)
640        );
641
642        let response = self.client.get(&url).send().await?;
643
644        self.handle_response(response).await
645    }
646
647    #[instrument(skip(self))]
648    pub async fn get_crawl_settings(&self, site_url: &str) -> Result<CrawlSettings> {
649        let url = format!(
650            "{}/json/GetCrawlSettings?apikey={}&siteUrl={}",
651            self.base_url,
652            self.api_key,
653            urlencoding::encode(site_url)
654        );
655
656        let response = self.client.get(&url).send().await?;
657
658        self.handle_response(response).await
659    }
660
661    #[instrument(skip(self))]
662    pub async fn get_deep_link(&self, site_url: &str, url: &str) -> Result<DeepLink> {
663        let api_url = format!(
664            "{}/json/GetDeepLink?apikey={}&siteUrl={}&url={}",
665            self.base_url,
666            self.api_key,
667            urlencoding::encode(site_url),
668            urlencoding::encode(url)
669        );
670
671        let response = self.client.get(&api_url).send().await?;
672
673        self.handle_response(response).await
674    }
675
676    #[instrument(skip(self))]
677    pub async fn get_deep_link_algo_urls(&self, site_url: &str) -> Result<Vec<DeepLinkAlgoUrl>> {
678        let url = format!(
679            "{}/json/GetDeepLinkAlgoUrls?apikey={}&siteUrl={}",
680            self.base_url,
681            self.api_key,
682            urlencoding::encode(site_url)
683        );
684
685        let response = self.client.get(&url).send().await?;
686
687        self.handle_response(response).await
688    }
689
690    #[instrument(skip(self))]
691    pub async fn get_deep_link_blocks(&self, site_url: &str) -> Result<Vec<DeepLinkBlock>> {
692        let url = format!(
693            "{}/json/GetDeepLinkBlocks?apikey={}&siteUrl={}",
694            self.base_url,
695            self.api_key,
696            urlencoding::encode(site_url)
697        );
698
699        let response = self.client.get(&url).send().await?;
700
701        self.handle_response(response).await
702    }
703
704    #[instrument(skip(self))]
705    pub async fn get_feed_details(&self, site_url: &str, feed_url: &str) -> Result<Vec<Feed>> {
706        let api_url = format!(
707            "{}/json/GetFeedDetails?apikey={}&siteUrl={}&feedUrl={}",
708            self.base_url,
709            self.api_key,
710            urlencoding::encode(site_url),
711            urlencoding::encode(feed_url)
712        );
713
714        let response = self.client.get(&api_url).send().await?;
715
716        self.handle_response(response).await
717    }
718
719    #[instrument(skip(self))]
720    pub async fn get_feeds(&self, site_url: &str) -> Result<Vec<Feed>> {
721        let url = format!(
722            "{}/json/GetFeeds?apikey={}&siteUrl={}",
723            self.base_url,
724            self.api_key,
725            urlencoding::encode(site_url)
726        );
727
728        let response = self.client.get(&url).send().await?;
729
730        self.handle_response(response).await
731    }
732
733    #[instrument(skip(self))]
734    pub async fn get_fetched_url_details(
735        &self,
736        site_url: &str,
737        url: &str,
738    ) -> Result<FetchedUrlDetails> {
739        let api_url = format!(
740            "{}/json/GetFetchedUrlDetails?apikey={}&siteUrl={}&url={}",
741            self.base_url,
742            self.api_key,
743            urlencoding::encode(site_url),
744            urlencoding::encode(url)
745        );
746
747        let response = self.client.get(&api_url).send().await?;
748
749        self.handle_response(response).await
750    }
751
752    #[instrument(skip(self))]
753    pub async fn get_fetched_urls(&self, site_url: &str) -> Result<Vec<FetchedUrl>> {
754        let url = format!(
755            "{}/json/GetFetchedUrls?apikey={}&siteUrl={}",
756            self.base_url,
757            self.api_key,
758            urlencoding::encode(site_url)
759        );
760
761        let response = self.client.get(&url).send().await?;
762
763        self.handle_response(response).await
764    }
765
766    #[instrument(skip(self))]
767    pub async fn get_keyword(
768        &self,
769        query: &str,
770        country: &str,
771        language: &str,
772        start_date: chrono::DateTime<chrono::Utc>,
773        end_date: chrono::DateTime<chrono::Utc>,
774    ) -> Result<Vec<Keyword>> {
775        let api_url = format!(
776            "{}/json/GetKeyword?apikey={}&q={}&country={}&language={}&startDate={}&endDate={}",
777            self.base_url,
778            self.api_key,
779            urlencoding::encode(query),
780            urlencoding::encode(country),
781            urlencoding::encode(language),
782            start_date.format("%Y-%m-%dT%H:%M:%S"),
783            end_date.format("%Y-%m-%dT%H:%M:%S")
784        );
785
786        let response = self.client.get(&api_url).send().await?;
787
788        self.handle_response(response).await
789    }
790
791    #[instrument(skip(self))]
792    pub async fn get_keyword_stats(
793        &self,
794        query: &str,
795        country: &str,
796        language: &str,
797    ) -> Result<Vec<KeywordStats>> {
798        let api_url = format!(
799            "{}/json/GetKeywordStats?apikey={}&q={}&country={}&language={}",
800            self.base_url,
801            self.api_key,
802            urlencoding::encode(query),
803            urlencoding::encode(country),
804            urlencoding::encode(language)
805        );
806
807        let response = self.client.get(&api_url).send().await?;
808
809        self.handle_response(response).await
810    }
811
812    #[instrument(skip(self))]
813    pub async fn get_link_counts(&self, site_url: &str, page: i16) -> Result<LinkCounts> {
814        let url = format!(
815            "{}/json/GetLinkCounts?apikey={}&siteUrl={}&page={}",
816            self.base_url,
817            self.api_key,
818            urlencoding::encode(site_url),
819            page
820        );
821
822        let response = self.client.get(&url).send().await?;
823
824        self.handle_response(response).await
825    }
826
827    #[instrument(skip(self))]
828    pub async fn get_page_query_stats(
829        &self,
830        site_url: &str,
831        page_url: &str,
832    ) -> Result<Vec<QueryStats>> {
833        let api_url = format!(
834            "{}/json/GetPageQueryStats?apikey={}&siteUrl={}&page={}",
835            self.base_url,
836            self.api_key,
837            urlencoding::encode(site_url),
838            urlencoding::encode(page_url)
839        );
840
841        let response = self.client.get(&api_url).send().await?;
842
843        self.handle_response(response).await
844    }
845
846    #[instrument(skip(self))]
847    pub async fn get_query_page_detail_stats(
848        &self,
849        site_url: &str,
850        query: &str,
851        page_url: &str,
852    ) -> Result<Vec<DetailedQueryStats>> {
853        let api_url = format!(
854            "{}/json/GetQueryPageDetailStats?apikey={}&siteUrl={}&query={}&page={}",
855            self.base_url,
856            self.api_key,
857            urlencoding::encode(site_url),
858            urlencoding::encode(query),
859            urlencoding::encode(page_url)
860        );
861
862        let response = self.client.get(&api_url).send().await?;
863
864        self.handle_response(response).await
865    }
866
867    #[instrument(skip(self))]
868    pub async fn get_query_page_stats(
869        &self,
870        site_url: &str,
871        query: &str,
872    ) -> Result<Vec<QueryStats>> {
873        let api_url = format!(
874            "{}/json/GetQueryPageStats?apikey={}&siteUrl={}&query={}",
875            self.base_url,
876            self.api_key,
877            urlencoding::encode(site_url),
878            urlencoding::encode(query)
879        );
880
881        let response = self.client.get(&api_url).send().await?;
882
883        self.handle_response(response).await
884    }
885
886    #[instrument(skip(self))]
887    pub async fn get_query_stats(&self, site_url: &str) -> Result<Vec<QueryStats>> {
888        let url = format!(
889            "{}/json/GetQueryStats?apikey={}&siteUrl={}",
890            self.base_url,
891            self.api_key,
892            urlencoding::encode(site_url)
893        );
894
895        let response = self.client.get(&url).send().await?;
896
897        self.handle_response(response).await
898    }
899
900    #[instrument(skip(self))]
901    pub async fn get_query_traffic_stats(
902        &self,
903        site_url: &str,
904        query: &str,
905    ) -> Result<Vec<RankAndTrafficStats>> {
906        let api_url = format!(
907            "{}/json/GetQueryTrafficStats?apikey={}&siteUrl={}&query={}",
908            self.base_url,
909            self.api_key,
910            urlencoding::encode(site_url),
911            urlencoding::encode(query)
912        );
913
914        let response = self.client.get(&api_url).send().await?;
915
916        self.handle_response(response).await
917    }
918
919    #[instrument(skip(self))]
920    pub async fn get_rank_and_traffic_stats(
921        &self,
922        site_url: &str,
923    ) -> Result<Vec<RankAndTrafficStats>> {
924        let url = format!(
925            "{}/json/GetRankAndTrafficStats?apikey={}&siteUrl={}",
926            self.base_url,
927            self.api_key,
928            urlencoding::encode(site_url)
929        );
930
931        let response = self.client.get(&url).send().await?;
932
933        self.handle_response(response).await
934    }
935
936    #[instrument(skip(self))]
937    pub async fn get_site_moves(&self, site_url: &str) -> Result<Vec<SiteMove>> {
938        let url = format!(
939            "{}/json/GetSiteMoves?apikey={}&siteUrl={}",
940            self.base_url,
941            self.api_key,
942            urlencoding::encode(site_url)
943        );
944
945        let response = self.client.get(&url).send().await?;
946
947        self.handle_response(response).await
948    }
949
950    #[instrument(skip(self))]
951    pub async fn get_site_roles(
952        &self,
953        site_url: &str,
954        include_all_subdomains: bool,
955    ) -> Result<Vec<SiteRoles>> {
956        let url = format!(
957            "{}/json/GetSiteRoles?apikey={}&siteUrl={}&includeAllSubdomains={}",
958            self.base_url,
959            self.api_key,
960            urlencoding::encode(site_url),
961            include_all_subdomains
962        );
963
964        let response = self.client.get(&url).send().await?;
965
966        self.handle_response(response).await
967    }
968
969    #[instrument(skip(self))]
970    pub async fn get_url_info(&self, site_url: &str, url: &str) -> Result<UrlInfo> {
971        let api_url = format!(
972            "{}/json/GetUrlInfo?apikey={}&siteUrl={}&url={}",
973            self.base_url,
974            self.api_key,
975            urlencoding::encode(site_url),
976            urlencoding::encode(url)
977        );
978
979        let response = self.client.get(&api_url).send().await?;
980
981        self.handle_response(response).await
982    }
983
984    #[instrument(skip(self))]
985    pub async fn get_url_links(
986        &self,
987        site_url: &str,
988        link: &str,
989        page: i16,
990    ) -> Result<LinkDetails> {
991        let api_url = format!(
992            "{}/json/GetUrlLinks?apikey={}&siteUrl={}&link={}&page={}",
993            self.base_url,
994            self.api_key,
995            urlencoding::encode(site_url),
996            urlencoding::encode(link),
997            page
998        );
999
1000        let response = self.client.get(&api_url).send().await?;
1001
1002        self.handle_response(response).await
1003    }
1004
1005    #[instrument(skip(self))]
1006    pub async fn get_url_submission_quota(&self, site_url: &str) -> Result<UrlSubmissionQuota> {
1007        let url = format!(
1008            "{}/json/GetUrlSubmissionQuota?apikey={}&siteUrl={}",
1009            self.base_url,
1010            self.api_key,
1011            urlencoding::encode(site_url)
1012        );
1013
1014        let response = self.client.get(&url).send().await?;
1015
1016        self.handle_response(response).await
1017    }
1018
1019    #[instrument(skip(self))]
1020    pub async fn get_url_traffic_info(&self, site_url: &str, url: &str) -> Result<UrlTrafficInfo> {
1021        let api_url = format!(
1022            "{}/json/GetUrlTrafficInfo?apikey={}&siteUrl={}&url={}",
1023            self.base_url,
1024            self.api_key,
1025            urlencoding::encode(site_url),
1026            urlencoding::encode(url)
1027        );
1028
1029        let response = self.client.get(&api_url).send().await?;
1030
1031        self.handle_response(response).await
1032    }
1033
1034    #[instrument(skip(self))]
1035    pub async fn get_user_sites(&self) -> Result<Vec<Site>> {
1036        let url = format!(
1037            "{}/json/GetUserSites?apikey={}",
1038            self.base_url, self.api_key
1039        );
1040
1041        let response = self.client.get(&url).send().await?;
1042
1043        self.handle_response(response).await
1044    }
1045
1046    // Remove/Delete methods
1047
1048    #[instrument(skip(self))]
1049    pub async fn remove_blocked_url(&self, site_url: &str, blocked_url: &BlockedUrl) -> Result<()> {
1050        let url = format!(
1051            "{}/json/RemoveBlockedUrl?apikey={}",
1052            self.base_url, self.api_key
1053        );
1054        let body = json!({
1055            "siteUrl": site_url,
1056            "blockedUrl": blocked_url
1057        });
1058
1059        let response = self
1060            .client
1061            .post(&url)
1062            .header("Content-Type", "application/json; charset=utf-8")
1063            .body(serde_json::to_string(&body)?)
1064            .send()
1065            .await?;
1066
1067        self.handle_void_response(response).await
1068    }
1069
1070    #[instrument(skip(self))]
1071    pub async fn remove_country_region_settings(
1072        &self,
1073        site_url: &str,
1074        settings: &CountryRegionSettings,
1075    ) -> Result<()> {
1076        let url = format!(
1077            "{}/json/RemoveCountryRegionSettings?apikey={}",
1078            self.base_url, self.api_key
1079        );
1080        let body = json!({
1081            "siteUrl": site_url,
1082            "settings": settings
1083        });
1084
1085        let response = self
1086            .client
1087            .post(&url)
1088            .header("Content-Type", "application/json; charset=utf-8")
1089            .body(serde_json::to_string(&body)?)
1090            .send()
1091            .await?;
1092
1093        self.handle_void_response(response).await
1094    }
1095
1096    #[instrument(skip(self))]
1097    pub async fn remove_deep_link_block(
1098        &self,
1099        site_url: &str,
1100        market: &str,
1101        search_url: &str,
1102        deep_link_url: &str,
1103    ) -> Result<()> {
1104        let url = format!(
1105            "{}/json/RemoveDeepLinkBlock?apikey={}",
1106            self.base_url, self.api_key
1107        );
1108        let body = json!({
1109            "siteUrl": site_url,
1110            "market": market,
1111            "searchUrl": search_url,
1112            "deepLinkUrl": deep_link_url
1113        });
1114
1115        let response = self
1116            .client
1117            .post(&url)
1118            .header("Content-Type", "application/json; charset=utf-8")
1119            .body(serde_json::to_string(&body)?)
1120            .send()
1121            .await?;
1122
1123        self.handle_void_response(response).await
1124    }
1125
1126    #[instrument(skip(self))]
1127    pub async fn remove_feed(&self, site_url: &str, feed_url: &str) -> Result<()> {
1128        let url = format!("{}/json/RemoveFeed?apikey={}", self.base_url, self.api_key);
1129        let body = json!({
1130            "siteUrl": site_url,
1131            "feedUrl": feed_url
1132        });
1133
1134        let response = self
1135            .client
1136            .post(&url)
1137            .header("Content-Type", "application/json; charset=utf-8")
1138            .body(serde_json::to_string(&body)?)
1139            .send()
1140            .await?;
1141
1142        self.handle_void_response(response).await
1143    }
1144
1145    #[instrument(skip(self))]
1146    pub async fn remove_query_parameter(
1147        &self,
1148        site_url: &str,
1149        query_parameter: &str,
1150    ) -> Result<()> {
1151        let url = format!(
1152            "{}/json/RemoveQueryParameter?apikey={}",
1153            self.base_url, self.api_key
1154        );
1155        let body = json!({
1156            "siteUrl": site_url,
1157            "queryParameter": query_parameter
1158        });
1159
1160        let response = self
1161            .client
1162            .post(&url)
1163            .header("Content-Type", "application/json; charset=utf-8")
1164            .body(serde_json::to_string(&body)?)
1165            .send()
1166            .await?;
1167
1168        self.handle_void_response(response).await
1169    }
1170
1171    #[instrument(skip(self))]
1172    pub async fn remove_site(&self, site_url: &str) -> Result<()> {
1173        let url = format!("{}/json/RemoveSite?apikey={}", self.base_url, self.api_key);
1174        let body = json!({
1175            "siteUrl": site_url
1176        });
1177
1178        let response = self
1179            .client
1180            .post(&url)
1181            .header("Content-Type", "application/json; charset=utf-8")
1182            .body(serde_json::to_string(&body)?)
1183            .send()
1184            .await?;
1185
1186        self.handle_void_response(response).await
1187    }
1188
1189    #[instrument(skip(self))]
1190    pub async fn remove_site_role(&self, site_url: &str, site_roles: &SiteRoles) -> Result<()> {
1191        let url = format!(
1192            "{}/json/RemoveSiteRole?apikey={}",
1193            self.base_url, self.api_key
1194        );
1195        let body = json!({
1196            "siteUrl": site_url,
1197            "siteRoles": site_roles
1198        });
1199
1200        let response = self
1201            .client
1202            .post(&url)
1203            .header("Content-Type", "application/json; charset=utf-8")
1204            .body(serde_json::to_string(&body)?)
1205            .send()
1206            .await?;
1207
1208        self.handle_void_response(response).await
1209    }
1210
1211    // Save/Submit methods
1212
1213    #[instrument(skip(self))]
1214    pub async fn save_crawl_settings(
1215        &self,
1216        site_url: &str,
1217        crawl_settings: &CrawlSettings,
1218    ) -> Result<()> {
1219        let url = format!(
1220            "{}/json/SaveCrawlSettings?apikey={}",
1221            self.base_url, self.api_key
1222        );
1223        let body = json!({
1224            "siteUrl": site_url,
1225            "crawlSettings": crawl_settings
1226        });
1227
1228        let response = self
1229            .client
1230            .post(&url)
1231            .header("Content-Type", "application/json; charset=utf-8")
1232            .body(serde_json::to_string(&body)?)
1233            .send()
1234            .await?;
1235
1236        self.handle_void_response(response).await
1237    }
1238
1239    #[instrument(skip(self))]
1240    pub async fn submit_feed(&self, site_url: &str, feed_url: &str) -> Result<()> {
1241        let url = format!("{}/json/SubmitFeed?apikey={}", self.base_url, self.api_key);
1242        let body = json!({
1243            "siteUrl": site_url,
1244            "feedUrl": feed_url
1245        });
1246
1247        let response = self
1248            .client
1249            .post(&url)
1250            .header("Content-Type", "application/json; charset=utf-8")
1251            .body(serde_json::to_string(&body)?)
1252            .send()
1253            .await?;
1254
1255        self.handle_void_response(response).await
1256    }
1257
1258    #[instrument(skip(self))]
1259    pub async fn submit_site_move(
1260        &self,
1261        site_url: &str,
1262        site_move_settings: &SiteMoveSettings,
1263    ) -> Result<()> {
1264        let url = format!(
1265            "{}/json/SubmitSiteMove?apikey={}",
1266            self.base_url, self.api_key
1267        );
1268        let body = json!({
1269            "siteUrl": site_url,
1270            "siteMoveSettings": site_move_settings
1271        });
1272
1273        let response = self
1274            .client
1275            .post(&url)
1276            .header("Content-Type", "application/json; charset=utf-8")
1277            .body(serde_json::to_string(&body)?)
1278            .send()
1279            .await?;
1280
1281        self.handle_void_response(response).await
1282    }
1283
1284    #[instrument(skip(self))]
1285    pub async fn update_deep_link(
1286        &self,
1287        site_url: &str,
1288        market: &str,
1289        search_url: &str,
1290        deep_link_weight: &DeepLinkWeight,
1291    ) -> Result<()> {
1292        let url = format!(
1293            "{}/json/UpdateDeepLink?apikey={}",
1294            self.base_url, self.api_key
1295        );
1296        let body = json!({
1297            "siteUrl": site_url,
1298            "market": market,
1299            "searchUrl": search_url,
1300            "deepLinkWeight": deep_link_weight
1301        });
1302
1303        let response = self
1304            .client
1305            .post(&url)
1306            .header("Content-Type", "application/json; charset=utf-8")
1307            .body(serde_json::to_string(&body)?)
1308            .send()
1309            .await?;
1310
1311        self.handle_void_response(response).await
1312    }
1313}