at main 4.0 kB view raw
1use data_encoding::BASE64URL; 2use reqwest::{ 3 IntoUrl, 4 header::{self, HeaderMap, HeaderValue}, 5}; 6use serde_json::Value; 7use url::Url; 8 9use crate::tap::{ 10 Error, HttpClient, RepoInfo, TapChannel, 11 channel::{Ack, ChannelError}, 12}; 13 14pub type Result<T> = core::result::Result<T, Error>; 15 16#[derive(Clone)] 17pub struct TapClient { 18 http: HttpClient, 19 url: Url, 20 pub(crate) headers: HeaderMap, 21} 22 23impl TapClient { 24 pub fn new<U: IntoUrl>(url: U, admin_password: Option<&str>) -> Result<Self> { 25 fn inner(url: Url, admin_password: Option<&str>) -> Result<TapClient> { 26 if !["http", "https"].contains(&url.scheme()) { 27 return Err(Error::UnsupportedScheme); 28 } 29 30 let mut headers = HeaderMap::new(); 31 if let Some(password) = admin_password { 32 let token = format!("admin:{password}"); 33 let encoded = BASE64URL.encode(token.as_bytes()); 34 headers.append( 35 header::AUTHORIZATION, 36 HeaderValue::from_str(&format!("Basic {encoded}"))?, 37 ); 38 } 39 40 let http = HttpClient::builder() 41 .default_headers(headers.clone()) 42 .user_agent(crate::tap::USER_AGENT) 43 .build()?; 44 45 Ok(TapClient { http, url, headers }) 46 } 47 48 inner(url.into_url()?, admin_password) 49 } 50 51 pub fn url(&self) -> &Url { 52 &self.url 53 } 54} 55 56impl TapClient { 57 /// Create a WebSocket channel to receive events. 58 pub fn channel( 59 &self, 60 ) -> ( 61 TapChannel, 62 impl Future<Output = core::result::Result<Vec<Ack>, ChannelError>> + Send + 'static, 63 ) { 64 TapChannel::new(self, TapChannel::DEFAULT_CAPACITY) 65 } 66 67 /// Start tracking repos. 68 /// 69 /// This will trigger backfill. 70 pub async fn add_repos(&self, dids: &[&str]) -> Result<()> { 71 #[derive(serde::Serialize)] 72 struct RequestBody<'a> { 73 dids: &'a [&'a str], 74 } 75 76 // An empty slice will trigger an internal service error in the Tap service. 77 if dids.is_empty() { 78 return Ok(()); 79 } 80 81 let mut url = self.url.clone(); 82 url.set_path("/repos/add"); 83 84 let response = self 85 .http 86 .post(url) 87 .json(&RequestBody { dids }) 88 .send() 89 .await? 90 .error_for_status()?; 91 92 let body = response.bytes().await?; 93 assert!(body.is_empty(), "expected an empty response body"); 94 95 Ok(()) 96 } 97 98 /// Stop tracking repos. 99 pub async fn remove_repos(&self, dids: &[&str]) -> Result<()> { 100 #[derive(serde::Serialize)] 101 struct RequestBody<'a> { 102 dids: &'a [&'a str], 103 } 104 105 let mut url = self.url.clone(); 106 url.set_path("/repos/remove"); 107 108 let response = self 109 .http 110 .post(url) 111 .json(&RequestBody { dids }) 112 .send() 113 .await? 114 .error_for_status()?; 115 116 let body = response.bytes().await?; 117 assert!(body.is_empty(), "expected an empty response body"); 118 119 Ok(()) 120 } 121 122 /// Resolve a DID to its DID document. 123 pub async fn resolve_did(&self, did: &str) -> Result<Value> { 124 let mut url = self.url.clone(); 125 url.set_path(&format!("/resolve/{did}")); 126 127 let response = self 128 .http 129 .get(url) 130 .send() 131 .await? 132 .error_for_status()? 133 .json() 134 .await?; 135 136 Ok(response) 137 } 138 139 /// Get info about a tracked repo. 140 pub async fn get_repo_info(&self, did: &str) -> Result<RepoInfo> { 141 let mut url = self.url.clone(); 142 url.set_path(&format!("/info/{did}")); 143 144 let response = self 145 .http 146 .get(url) 147 .send() 148 .await? 149 .error_for_status()? 150 .json() 151 .await?; 152 153 Ok(response) 154 } 155}