use data_encoding::BASE64URL; use reqwest::{ IntoUrl, header::{self, HeaderMap, HeaderValue}, }; use serde_json::Value; use url::Url; use crate::tap::{Error, HttpClient, RepoInfo, TapChannel, channel::ChannelError}; pub type Result = core::result::Result; #[derive(Clone)] pub struct TapClient { http: HttpClient, url: Url, pub(crate) headers: HeaderMap, } impl TapClient { pub fn new(url: U, admin_password: Option<&str>) -> Result { fn inner(url: Url, admin_password: Option<&str>) -> Result { if !["http", "https"].contains(&url.scheme()) { return Err(Error::UnsupportedScheme); } let mut headers = HeaderMap::new(); if let Some(password) = admin_password { let token = format!("admin:{password}"); let encoded = BASE64URL.encode(token.as_bytes()); headers.append( header::AUTHORIZATION, HeaderValue::from_str(&format!("Basic {encoded}"))?, ); } let http = HttpClient::builder() .default_headers(headers.clone()) .user_agent(crate::tap::USER_AGENT) .build()?; Ok(TapClient { http, url, headers }) } inner(url.into_url()?, admin_password) } pub fn url(&self) -> &Url { &self.url } } impl TapClient { /// Create a WebSocket channel to receive events. pub fn channel( &self, ) -> ( TapChannel, impl Future> + Send + 'static, ) { TapChannel::new(self, TapChannel::DEFAULT_CAPACITY) } /// Start tracking repos. /// /// This will trigger backfill. pub async fn add_repos(&self, dids: &[&str]) -> Result<()> { #[derive(serde::Serialize)] struct RequestBody<'a> { dids: &'a [&'a str], } // An empty slice will trigger an internal service error in the Tap service. if dids.is_empty() { return Ok(()); } let mut url = self.url.clone(); url.set_path("/repos/add"); let response = self .http .post(url) .json(&RequestBody { dids }) .send() .await? .error_for_status()?; let body = response.bytes().await?; assert!(body.is_empty(), "expected an empty response body"); Ok(()) } /// Stop tracking repos. pub async fn remove_repos(&self, dids: &[&str]) -> Result<()> { #[derive(serde::Serialize)] struct RequestBody<'a> { dids: &'a [&'a str], } let mut url = self.url.clone(); url.set_path("/repos/remove"); let response = self .http .post(url) .json(&RequestBody { dids }) .send() .await? .error_for_status()?; let body = response.bytes().await?; assert!(body.is_empty(), "expected an empty response body"); Ok(()) } /// Resolve a DID to its DID document. pub async fn resolve_did(&self, did: &str) -> Result { let mut url = self.url.clone(); url.set_path(&format!("/resolve/{did}")); let response = self .http .get(url) .send() .await? .error_for_status()? .json() .await?; Ok(response) } /// Get info about a tracked repo. pub async fn get_repo_info(&self, did: &str) -> Result { let mut url = self.url.clone(); url.set_path(&format!("/info/{did}")); let response = self .http .get(url) .send() .await? .error_for_status()? .json() .await?; Ok(response) } }