pubky/native/
client.rs

1use std::fmt::Debug;
2
3#[cfg(not(wasm_browser))]
4use super::internal::cookies::CookieJar;
5#[cfg(not(wasm_browser))]
6use std::sync::Arc;
7use std::time::Duration;
8
9static DEFAULT_USER_AGENT: &str = concat!("pubky.org", "@", env!("CARGO_PKG_VERSION"),);
10
11static DEFAULT_RELAYS: &[&str] = &["https://pkarr.pubky.org/", "https://pkarr.pubky.app/"];
12
13#[macro_export]
14macro_rules! handle_http_error {
15    ($res:expr) => {
16        if let Err(status) = $res.error_for_status_ref() {
17            return match $res.text().await {
18                Ok(text) => Err(anyhow::anyhow!("{status}. Error message: {text}")),
19                _ => Err(anyhow::anyhow!("{status}")),
20            };
21        }
22    };
23}
24
25#[derive(Debug, Default, Clone)]
26pub struct ClientBuilder {
27    pkarr: pkarr::ClientBuilder,
28    http_request_timeout: Option<Duration>,
29    /// Maximum age before a user record should be republished.
30    /// Defaults to 1 hour.
31    max_record_age: Option<Duration>,
32}
33
34impl ClientBuilder {
35    #[cfg(not(wasm_browser))]
36    /// Creates a client connected to a local test network with hardcoded configurations:
37    /// 1. local DHT with bootstrapping nodes: `&["localhost:6881"]`
38    /// 2. Pkarr Relay running on port [15411][pubky_common::constants::testnet_ports::PKARR_RELAY]
39    pub fn testnet(&mut self) -> &mut Self {
40        self.pkarr
41            .bootstrap(&[format!(
42                "localhost:{}",
43                pubky_common::constants::testnet_ports::BOOTSTRAP
44            )])
45            .relays(&[format!(
46                "http://localhost:{}",
47                pubky_common::constants::testnet_ports::PKARR_RELAY
48            )])
49            .expect("relays urls infallible");
50
51        self
52    }
53
54    /// Allows mutating the internal [pkarr::ClientBuilder] with a callback function.
55    pub fn pkarr<F>(&mut self, f: F) -> &mut Self
56    where
57        F: FnOnce(&mut pkarr::ClientBuilder) -> &mut pkarr::ClientBuilder,
58    {
59        f(&mut self.pkarr);
60
61        self
62    }
63
64    /// Set HTTP requests timeout.
65    pub fn request_timeout(&mut self, timeout: Duration) -> &mut Self {
66        self.http_request_timeout = Some(timeout);
67
68        self
69    }
70
71    /// Set max age a record can have before it must be republished.
72    /// Defaults to 1 hour if not overridden.
73    pub fn max_record_age(&mut self, max_age: Duration) -> &mut Self {
74        self.max_record_age = Some(max_age);
75        self
76    }
77
78    /// Build [Client]
79    pub fn build(&self) -> Result<Client, BuildError> {
80        let pkarr = self.pkarr.build()?;
81
82        #[cfg(not(wasm_browser))]
83        let cookie_store = Arc::new(CookieJar::default());
84
85        // TODO: allow custom user agent, but force a Pubky user agent information
86        let user_agent = DEFAULT_USER_AGENT;
87
88        #[cfg(not(wasm_browser))]
89        let mut http_builder = reqwest::ClientBuilder::from(pkarr.clone())
90            // TODO: use persistent cookie jar
91            .cookie_provider(cookie_store.clone())
92            .user_agent(user_agent);
93
94        #[cfg(wasm_browser)]
95        let http_builder = reqwest::Client::builder().user_agent(user_agent);
96
97        #[cfg(not(wasm_browser))]
98        let mut icann_http_builder = reqwest::Client::builder()
99            // TODO: use persistent cookie jar
100            .cookie_provider(cookie_store.clone())
101            .user_agent(user_agent);
102
103        // TODO: change this after Reqwest publish a release with timeout in wasm
104        #[cfg(not(wasm_browser))]
105        if let Some(timeout) = self.http_request_timeout {
106            http_builder = http_builder.timeout(timeout);
107
108            icann_http_builder = icann_http_builder.timeout(timeout);
109        }
110
111        // Maximum age before a homeserver record should be republished.
112        // Default is 1 hour. It's an arbitrary decision based only anecdotal evidence for DHT eviction.
113        // See https://github.com/pubky/pkarr-churn/blob/main/results-node_decay.md for latest date of record churn
114        let max_record_age = self.max_record_age.unwrap_or(Duration::from_secs(60 * 60));
115
116        Ok(Client {
117            pkarr,
118            http: http_builder.build().expect("config expected to not error"),
119
120            #[cfg(not(wasm_browser))]
121            icann_http: icann_http_builder
122                .build()
123                .expect("config expected to not error"),
124            #[cfg(not(wasm_browser))]
125            cookie_store,
126
127            #[cfg(wasm_browser)]
128            testnet: false,
129
130            max_record_age,
131        })
132    }
133}
134
135#[derive(Debug, thiserror::Error)]
136pub enum BuildError {
137    #[error(transparent)]
138    /// Error building Pkarr client.
139    PkarrBuildError(#[from] pkarr::errors::BuildError),
140}
141
142/// A client for Pubky homeserver API, as well as generic HTTP requests to Pubky urls.
143#[derive(Clone, Debug)]
144pub struct Client {
145    pub(crate) http: reqwest::Client,
146    pub(crate) pkarr: pkarr::Client,
147
148    #[cfg(not(wasm_browser))]
149    pub(crate) cookie_store: std::sync::Arc<CookieJar>,
150    #[cfg(not(wasm_browser))]
151    pub(crate) icann_http: reqwest::Client,
152
153    #[cfg(wasm_browser)]
154    pub(crate) testnet: bool,
155
156    /// The record age threshold before republishing.
157    pub(crate) max_record_age: Duration,
158}
159
160impl Client {
161    /// Returns a builder to edit settings before creating [Client].
162    pub fn builder() -> ClientBuilder {
163        let mut builder = ClientBuilder::default();
164        builder.pkarr(|pkarr| pkarr.relays(DEFAULT_RELAYS).expect("infallible"));
165        builder
166    }
167
168    // === Getters ===
169
170    /// Returns a reference to the internal Pkarr Client.
171    pub fn pkarr(&self) -> &pkarr::Client {
172        &self.pkarr
173    }
174}
175
176#[cfg(not(wasm_browser))]
177#[cfg(test)]
178mod test {
179    use super::*;
180
181    #[tokio::test]
182    async fn test_fetch() {
183        let client = Client::builder().build().unwrap();
184        let response = client.get("https://google.com/").send().await.unwrap();
185        assert_eq!(response.status(), 200);
186    }
187}