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}