1use reqwest_middleware::ClientBuilder;
2use serde_json::json;
3use serde_qs::ArrayFormat;
4use tracing::instrument;
5
6use crate::{
7 dto::*,
8 error::{Result, YandexApiErrorResponse, YandexWebmasterError},
9 middleware::AuthMiddleware,
10};
11
12const API_BASE_URL: &str = "https://api.webmaster.yandex.net/v4";
14
15#[derive(Debug, Clone)]
17pub struct YandexWebmasterClient {
18 client: reqwest_middleware::ClientWithMiddleware,
19 user_id: i64,
20 qs: serde_qs::Config,
21}
22
23impl YandexWebmasterClient {
24 pub fn oauth_url(client_id: &str) -> String {
26 format!("https://oauth.yandex.ru/authorize?response_type=token&client_id={client_id}")
27 }
28
29 #[instrument(skip(oauth_token))]
42 pub async fn new(oauth_token: String) -> Result<Self> {
43 let client = ClientBuilder::new(reqwest::Client::new())
45 .with(AuthMiddleware::new(oauth_token))
46 .build();
47
48 let user_response = Self::fetch_user(&client).await?;
50
51 tracing::info!(
52 user_id = user_response.user_id,
53 "Successfully authenticated"
54 );
55
56 Ok(Self {
57 client,
58 user_id: user_response.user_id,
59 qs: serde_qs::Config::new().array_format(ArrayFormat::Unindexed),
60 })
61 }
62
63 #[instrument(skip(oauth_token, client))]
77 pub async fn with_client(oauth_token: String, client: ClientBuilder) -> Result<Self> {
78 let client = client.with(AuthMiddleware::new(oauth_token)).build();
80
81 let user_response = Self::fetch_user(&client).await?;
83
84 tracing::info!(
85 user_id = user_response.user_id,
86 "Successfully authenticated"
87 );
88
89 Ok(Self {
90 client,
91 user_id: user_response.user_id,
92 qs: serde_qs::Config::new().array_format(ArrayFormat::Unindexed),
93 })
94 }
95
96 #[instrument(skip(client))]
98 async fn fetch_user(client: &reqwest_middleware::ClientWithMiddleware) -> Result<UserResponse> {
99 let url = format!("{}/user", API_BASE_URL);
100
101 tracing::debug!(url = %url, "Fetching user information");
102
103 let response = client.get(&url).send().await?;
104
105 if !response.status().is_success() {
106 return Err(Self::parse_error(response).await);
107 }
108
109 let user_response: UserResponse = response.json().await?;
110
111 Ok(user_response)
112 }
113
114 pub fn user_id(&self) -> i64 {
116 self.user_id
117 }
118
119 #[instrument(skip(self))]
125 pub async fn get_hosts(&self) -> Result<Vec<HostInfo>> {
126 let url = format!("{}/user/{}/hosts", API_BASE_URL, self.user_id);
127 let result: HostsResponse = self.get(&url).await?;
128 Ok(result.hosts)
129 }
130
131 #[instrument(skip(self))]
133 pub async fn add_host(
134 &self,
135 host_url: &str,
136 verification_type: VerificationType,
137 ) -> Result<AddHostResponse> {
138 let url = format!("{}/user/{}/hosts", API_BASE_URL, self.user_id);
139 self.post(
140 &url,
141 &json!({ "host_url": host_url.to_string(), "verification_type": verification_type }),
142 )
143 .await
144 }
145
146 #[instrument(skip(self))]
148 pub async fn get_host(&self, host_id: &str) -> Result<FullHostInfo> {
149 let url = format!("{}/user/{}/hosts/{}", API_BASE_URL, self.user_id, host_id);
150 self.get(&url).await
151 }
152
153 #[instrument(skip(self))]
155 pub async fn delete_host(&self, host_id: &str) -> Result<()> {
156 let url = format!("{}/user/{}/hosts/{}", API_BASE_URL, self.user_id, host_id);
157 self.delete(&url).await
158 }
159
160 #[instrument(skip(self))]
166 pub async fn get_verification_status(&self, host_id: &str) -> Result<HostVerificationResponse> {
167 let url = format!(
168 "{}/user/{}/hosts/{}/verification",
169 API_BASE_URL, self.user_id, host_id
170 );
171 self.get(&url).await
172 }
173
174 #[instrument(skip(self))]
176 pub async fn verify_host(
177 &self,
178 host_id: &str,
179 verification_type: ExplicitVerificationType,
180 ) -> Result<HostVerificationResponse> {
181 let serialized = serde_json::to_value(verification_type)?;
182 let verification_type = serialized.clone().as_str().unwrap_or("").to_owned();
183
184 let url = format!(
185 "{}/user/{}/hosts/{}/verification?verification_type={}",
186 API_BASE_URL, self.user_id, host_id, verification_type
187 );
188 self.post(&url, &()).await
189 }
190
191 #[instrument(skip(self))]
193 pub async fn get_owners(&self, host_id: &str) -> Result<Vec<Owner>> {
194 let url = format!(
195 "{}/user/{}/hosts/{}/owners",
196 API_BASE_URL, self.user_id, host_id
197 );
198 let result: OwnersResponse = self.get(&url).await?;
199 Ok(result.users)
200 }
201
202 #[instrument(skip(self))]
208 pub async fn get_host_summary(&self, host_id: &str) -> Result<HostSummaryResponse> {
209 let url = format!(
210 "{}/user/{}/hosts/{}/summary",
211 API_BASE_URL, self.user_id, host_id
212 );
213 self.get(&url).await
214 }
215
216 #[instrument(skip(self))]
218 pub async fn get_sqi_history(
219 &self,
220 host_id: &str,
221 req: SqiHistoryRequest,
222 ) -> Result<Vec<SqiPoint>> {
223 let url = format!(
224 "{}/user/{}/hosts/{}/sqi-history?{}",
225 API_BASE_URL,
226 self.user_id,
227 host_id,
228 self.qs.serialize_string(&req)?
229 );
230 let result: SqiHistoryResponse = self.get(&url).await?;
231 Ok(result.points)
232 }
233
234 #[instrument(skip(self))]
240 pub async fn get_popular_queries(
241 &self,
242 host_id: &str,
243 request: &PopularQueriesRequest,
244 ) -> Result<PopularQueriesResponse> {
245 let url = format!(
246 "{}/user/{}/hosts/{}/search-queries/popular?{}",
247 API_BASE_URL,
248 self.user_id,
249 host_id,
250 self.qs.serialize_string(request)?
251 );
252 self.get(&url).await
253 }
254
255 #[instrument(skip(self))]
257 pub async fn get_query_analytics(
258 &self,
259 host_id: &str,
260 request: &QueryAnalyticsRequest,
261 ) -> Result<QueryAnalyticsResponse> {
262 let url = format!(
263 "{}/user/{}/hosts/{}/search-queries/all/history?{}",
264 API_BASE_URL,
265 self.user_id,
266 host_id,
267 self.qs.serialize_string(request)?
268 );
269 self.get(&url).await
270 }
271
272 #[instrument(skip(self))]
274 pub async fn get_query_history(
275 &self,
276 host_id: &str,
277 query_id: &str,
278 request: &QueryHistoryRequest,
279 ) -> Result<QueryHistoryResponse> {
280 let url = format!(
281 "{}/user/{}/hosts/{}/search-queries/{}/history?{}",
282 API_BASE_URL,
283 self.user_id,
284 host_id,
285 query_id,
286 self.qs.serialize_string(request)?
287 );
288 self.get(&url).await
289 }
290
291 #[instrument(skip(self))]
297 pub async fn get_sitemaps(
298 &self,
299 host_id: &str,
300 request: &GetSitemapsRequest,
301 ) -> Result<SitemapsResponse> {
302 let url = format!(
303 "{}/user/{}/hosts/{}/sitemaps?{}",
304 API_BASE_URL,
305 self.user_id,
306 host_id,
307 self.qs.serialize_string(request)?
308 );
309 self.get(&url).await
310 }
311
312 #[instrument(skip(self))]
314 pub async fn get_sitemap(&self, host_id: &str, sitemap_id: &str) -> Result<SitemapInfo> {
315 let url = format!(
316 "{}/user/{}/hosts/{}/sitemaps/{}",
317 API_BASE_URL, self.user_id, host_id, sitemap_id
318 );
319 self.get(&url).await
320 }
321
322 #[instrument(skip(self))]
324 pub async fn get_user_sitemaps(
325 &self,
326 host_id: &str,
327 request: &GetUserSitemapsRequest,
328 ) -> Result<UserSitemapsResponse> {
329 let url = format!(
330 "{}/user/{}/hosts/{}/user-added-sitemaps?{}",
331 API_BASE_URL,
332 self.user_id,
333 host_id,
334 self.qs.serialize_string(request)?
335 );
336 self.get(&url).await
337 }
338
339 #[instrument(skip(self))]
341 pub async fn add_sitemap(&self, host_id: &str, url: &str) -> Result<AddSitemapResponse> {
342 let body = json!({ "url": url.to_string() });
343 let url = format!(
344 "{}/user/{}/hosts/{}/user-added-sitemaps",
345 API_BASE_URL, self.user_id, host_id
346 );
347 self.post(&url, &body).await
348 }
349
350 #[instrument(skip(self))]
352 pub async fn get_user_sitemap(
353 &self,
354 host_id: &str,
355 sitemap_id: &str,
356 ) -> Result<UserSitemapInfo> {
357 let url = format!(
358 "{}/user/{}/hosts/{}/user-added-sitemaps/{}",
359 API_BASE_URL, self.user_id, host_id, sitemap_id
360 );
361 self.get(&url).await
362 }
363
364 #[instrument(skip(self))]
366 pub async fn delete_sitemap(&self, host_id: &str, sitemap_id: &str) -> Result<()> {
367 let url = format!(
368 "{}/user/{}/hosts/{}/user-added-sitemaps/{}",
369 API_BASE_URL, self.user_id, host_id, sitemap_id
370 );
371 self.delete(&url).await
372 }
373
374 #[instrument(skip(self))]
380 pub async fn get_indexing_history(
381 &self,
382 host_id: &str,
383 request: &IndexingHistoryRequest,
384 ) -> Result<IndexingHistoryResponse> {
385 let url = format!(
386 "{}/user/{}/hosts/{}/indexing/history?{}",
387 API_BASE_URL,
388 self.user_id,
389 host_id,
390 self.qs.serialize_string(request)?
391 );
392 self.get(&url).await
393 }
394
395 #[instrument(skip(self))]
397 pub async fn get_indexing_samples(
398 &self,
399 host_id: &str,
400 request: &GetIndexingSamplesRequest,
401 ) -> Result<IndexingSamplesResponse> {
402 let url = format!(
403 "{}/user/{}/hosts/{}/indexing/samples?{}",
404 API_BASE_URL,
405 self.user_id,
406 host_id,
407 self.qs.serialize_string(request)?
408 );
409 self.get(&url).await
410 }
411
412 #[instrument(skip(self))]
414 pub async fn get_search_urls_history(
415 &self,
416 host_id: &str,
417 request: &IndexingHistoryRequest,
418 ) -> Result<SearchUrlsHistoryResponse> {
419 let url = format!(
420 "{}/user/{}/hosts/{}/search-urls/in-search/history?{}",
421 API_BASE_URL,
422 self.user_id,
423 host_id,
424 self.qs.serialize_string(request)?
425 );
426 self.get(&url).await
427 }
428
429 #[instrument(skip(self))]
431 pub async fn get_search_urls_samples(
432 &self,
433 host_id: &str,
434 request: &GetSearchUrlsSamplesRequest,
435 ) -> Result<SearchUrlsSamplesResponse> {
436 let url = format!(
437 "{}/user/{}/hosts/{}/search-urls/in-search/samples?{}",
438 API_BASE_URL,
439 self.user_id,
440 host_id,
441 self.qs.serialize_string(request)?
442 );
443 self.get(&url).await
444 }
445
446 #[instrument(skip(self))]
448 pub async fn get_search_events_history(
449 &self,
450 host_id: &str,
451 request: &IndexingHistoryRequest,
452 ) -> Result<SearchEventsHistoryResponse> {
453 let url = format!(
454 "{}/user/{}/hosts/{}/search-urls/events/history?{}",
455 API_BASE_URL,
456 self.user_id,
457 host_id,
458 self.qs.serialize_string(request)?
459 );
460 self.get(&url).await
461 }
462
463 #[instrument(skip(self))]
465 pub async fn get_search_events_samples(
466 &self,
467 host_id: &str,
468 request: &GetSearchEventsSamplesRequest,
469 ) -> Result<SearchEventsSamplesResponse> {
470 let url = format!(
471 "{}/user/{}/hosts/{}/search-urls/events/samples?{}",
472 API_BASE_URL,
473 self.user_id,
474 host_id,
475 self.qs.serialize_string(request)?
476 );
477 self.get(&url).await
478 }
479
480 #[instrument(skip(self))]
486 pub async fn get_important_urls(&self, host_id: &str) -> Result<ImportantUrlsResponse> {
487 let url = format!(
488 "{}/user/{}/hosts/{}/important-urls",
489 API_BASE_URL, self.user_id, host_id
490 );
491 self.get(&url).await
492 }
493
494 #[instrument(skip(self))]
496 pub async fn get_important_urls_history(
497 &self,
498 host_id: &str,
499 url_param: &str,
500 ) -> Result<ImportantUrlHistoryResponse> {
501 let url = format!(
502 "{}/user/{}/hosts/{}/important-urls/history?url={}",
503 API_BASE_URL,
504 self.user_id,
505 host_id,
506 urlencoding::encode(url_param)
507 );
508 self.get(&url).await
509 }
510
511 #[instrument(skip(self))]
517 pub async fn recrawl_urls(&self, host_id: &str, url: &str) -> Result<RecrawlResponse> {
518 let body = json!({ "url": url });
519 let url = format!(
520 "{}/user/{}/hosts/{}/recrawl/queue",
521 API_BASE_URL, self.user_id, host_id
522 );
523 self.post(&url, &body).await
524 }
525
526 #[instrument(skip(self))]
528 pub async fn get_recrawl_tasks(
529 &self,
530 host_id: &str,
531 request: &GetRecrawlTasksRequest,
532 ) -> Result<RecrawlTasksResponse> {
533 let url = format!(
534 "{}/user/{}/hosts/{}/recrawl/queue?{}",
535 API_BASE_URL,
536 self.user_id,
537 host_id,
538 self.qs.serialize_string(request)?
539 );
540 self.get(&url).await
541 }
542
543 #[instrument(skip(self))]
545 pub async fn get_recrawl_task(&self, host_id: &str, task_id: &str) -> Result<RecrawlTask> {
546 let url = format!(
547 "{}/user/{}/hosts/{}/recrawl/queue/{}",
548 API_BASE_URL, self.user_id, host_id, task_id
549 );
550 self.get(&url).await
551 }
552
553 #[instrument(skip(self))]
555 pub async fn get_recrawl_quota(&self, host_id: &str) -> Result<RecrawlQuotaResponse> {
556 let url = format!(
557 "{}/user/{}/hosts/{}/recrawl/quota",
558 API_BASE_URL, self.user_id, host_id
559 );
560 self.get(&url).await
561 }
562
563 #[instrument(skip(self))]
569 pub async fn get_broken_links(
570 &self,
571 host_id: &str,
572 request: &BrokenLinksRequest,
573 ) -> Result<BrokenLinksResponse> {
574 let url = format!(
575 "{}/user/{}/hosts/{}/links/internal/broken/samples?{}",
576 API_BASE_URL,
577 self.user_id,
578 host_id,
579 self.qs.serialize_string(request)?
580 );
581 self.get(&url).await
582 }
583
584 #[instrument(skip(self))]
586 pub async fn get_broken_links_history(
587 &self,
588 host_id: &str,
589 request: &BrokenLinkHistoryRequest,
590 ) -> Result<BrokenLinkHistoryResponse> {
591 let url = format!(
592 "{}/user/{}/hosts/{}/links/internal/broken/history?{}",
593 API_BASE_URL,
594 self.user_id,
595 host_id,
596 self.qs.serialize_string(request)?
597 );
598 self.get(&url).await
599 }
600
601 #[instrument(skip(self))]
603 pub async fn get_external_links(
604 &self,
605 host_id: &str,
606 request: &ExternalLinksRequest,
607 ) -> Result<ExternalLinksResponse> {
608 let url = format!(
609 "{}/user/{}/hosts/{}/links/external/samples?{}",
610 API_BASE_URL,
611 self.user_id,
612 host_id,
613 self.qs.serialize_string(request)?
614 );
615 self.get(&url).await
616 }
617
618 #[instrument(skip(self))]
620 pub async fn get_external_links_history(
621 &self,
622 host_id: &str,
623 ) -> Result<ExternalLinksHistoryResponse> {
624 let url = format!(
625 "{}/user/{}/hosts/{}/links/external/history?indicator=LINKS_TOTAL_COUNT",
626 API_BASE_URL, self.user_id, host_id
627 );
628 self.get(&url).await
629 }
630
631 #[instrument(skip(self))]
637 pub async fn get_diagnostics(&self, host_id: &str) -> Result<DiagnosticsResponse> {
638 let url = format!(
639 "{}/user/{}/hosts/{}/diagnostics",
640 API_BASE_URL, self.user_id, host_id
641 );
642 self.get(&url).await
643 }
644
645 #[instrument(skip(self))]
651 async fn get<T: serde::de::DeserializeOwned>(&self, url: &str) -> Result<T> {
652 tracing::debug!(url = %url, "Making GET request");
653
654 let response = self.client.get(url).send().await?;
655
656 Self::handle_response(response).await
657 }
658
659 #[instrument(skip(self, body))]
661 async fn post<B: serde::Serialize, T: serde::de::DeserializeOwned>(
662 &self,
663 url: &str,
664 body: &B,
665 ) -> Result<T> {
666 tracing::debug!(url = %url, "Making POST request");
667
668 let json_body = serde_json::to_string(body)?;
669
670 let response = self
671 .client
672 .post(url)
673 .header("Content-Type", "application/json")
674 .body(json_body)
675 .send()
676 .await?;
677
678 Self::handle_response(response).await
679 }
680
681 #[instrument(skip(self))]
683 async fn delete(&self, url: &str) -> Result<()> {
684 tracing::debug!(url = %url, "Making DELETE request");
685
686 let response = self.client.delete(url).send().await?;
687
688 if !response.status().is_success() {
689 return Err(Self::parse_error(response).await);
690 }
691
692 Ok(())
693 }
694
695 #[instrument(skip(response))]
697 async fn parse_error(response: reqwest::Response) -> YandexWebmasterError {
698 let status = response.status();
699 let status_code = status.as_u16();
700
701 match response.text().await {
703 Ok(error_text) => {
704 match serde_json::from_str::<YandexApiErrorResponse>(&error_text) {
706 Ok(api_error) => {
707 tracing::error!(
708 status = %status,
709 error_code = %api_error.error_code,
710 error_message = %api_error.error_message,
711 "Structured API error"
712 );
713 YandexWebmasterError::ApiError {
714 status: status_code,
715 response: api_error,
716 }
717 }
718 Err(_) => {
719 tracing::error!(
721 status = %status,
722 error = %error_text,
723 "API request failed with unstructured error"
724 );
725 YandexWebmasterError::GenericApiError(format!(
726 "Status: {}, Error: {}",
727 status, error_text
728 ))
729 }
730 }
731 }
732 Err(e) => {
733 tracing::error!(
734 status = %status,
735 error = %e,
736 "Failed to read error response"
737 );
738 YandexWebmasterError::GenericApiError(format!(
739 "Status: {}, Failed to read error response: {}",
740 status, e
741 ))
742 }
743 }
744 }
745
746 #[instrument(skip(response))]
748 async fn handle_response<T: serde::de::DeserializeOwned>(
749 response: reqwest::Response,
750 ) -> Result<T> {
751 if !response.status().is_success() {
752 return Err(Self::parse_error(response).await);
753 }
754
755 let data: T = response.json().await?;
756 Ok(data)
757 }
758}