Skip to main content

atrium_api/
agent.rs

1//! Structs and traits for managing sessions and making the XRPC requests.
2
3pub mod atp_agent;
4#[cfg(feature = "bluesky")]
5pub mod bluesky;
6mod inner;
7mod session_manager;
8pub mod utils;
9
10pub use self::session_manager::SessionManager;
11use crate::{client::Service, types::string::Did};
12use atrium_xrpc::types::AuthorizationToken;
13use std::{future::Future, sync::Arc};
14
15/// A trait for providing authorization tokens.
16#[cfg_attr(not(target_arch = "wasm32"), trait_variant::make(Send))]
17pub trait AuthorizationProvider {
18    #[allow(unused_variables)]
19    fn authorization_token(
20        &self,
21        is_refresh: bool,
22    ) -> impl Future<Output = Option<AuthorizationToken>>;
23}
24
25/// A trait for configuring the endpoint and headers of a client.
26pub trait Configure {
27    /// Set the current endpoint.
28    fn configure_endpoint(&self, endpoint: String);
29    /// Configures the moderation services to be applied on requests.
30    fn configure_labelers_header(&self, labeler_dids: Option<Vec<(Did, bool)>>);
31    /// Configures the atproto-proxy header to be applied on requests.
32    fn configure_proxy_header(&self, did: Did, service_type: impl AsRef<str>);
33}
34
35/// A trait for cloning a client with a proxy header.
36pub trait CloneWithProxy {
37    fn clone_with_proxy(&self, did: Did, service_type: impl AsRef<str>) -> Self;
38}
39
40/// Supported proxy targets.
41#[cfg(feature = "bluesky")]
42pub type AtprotoServiceType = self::bluesky::AtprotoServiceType;
43
44#[cfg(not(feature = "bluesky"))]
45pub enum AtprotoServiceType {
46    AtprotoLabeler,
47}
48
49#[cfg(not(feature = "bluesky"))]
50impl AsRef<str> for AtprotoServiceType {
51    fn as_ref(&self) -> &str {
52        match self {
53            Self::AtprotoLabeler => "atproto_labeler",
54        }
55    }
56}
57
58/// A wrapper around a session manager and a client service.
59///
60/// An agent provides the following utilities:
61/// - AT Protocol labelers configuration utilities
62/// - AT Protocol proxy configuration utilities
63/// - Cloning utilities (if the session manager implements [`CloneWithProxy`])
64///
65/// # Example
66///
67/// ```
68/// use atrium_api::agent::atp_agent::{store::MemorySessionStore, CredentialSession};
69/// use atrium_api::agent::Agent;
70/// use atrium_xrpc_client::reqwest::ReqwestClient;
71///
72/// let session = CredentialSession::new(
73///     ReqwestClient::new("https://bsky.social"),
74///     MemorySessionStore::default(),
75/// );
76/// let agent = Agent::new(session);
77/// ``````
78pub struct Agent<M>
79where
80    M: SessionManager + Send + Sync,
81{
82    session_manager: Arc<inner::Wrapper<M>>,
83    pub api: Service<inner::Wrapper<M>>,
84}
85
86impl<M> Agent<M>
87where
88    M: SessionManager + Send + Sync,
89{
90    /// Creates a new agent with the given session manager.
91    pub fn new(session_manager: M) -> Self {
92        let session_manager = Arc::new(inner::Wrapper::new(session_manager));
93        let api = Service::new(session_manager.clone());
94        Self { session_manager, api }
95    }
96    /// Returns the DID of the current session.
97    pub async fn did(&self) -> Option<Did> {
98        self.session_manager.did().await
99    }
100}
101
102impl<M> Agent<M>
103where
104    M: CloneWithProxy + SessionManager + Send + Sync,
105{
106    /// Configures the atproto-proxy header to be applied on requests.
107    ///
108    /// Returns a new client service with the proxy header configured.
109    pub fn api_with_proxy(
110        &self,
111        did: Did,
112        service_type: impl AsRef<str>,
113    ) -> Service<inner::Wrapper<M>> {
114        Service::new(Arc::new(self.session_manager.clone_with_proxy(did, service_type)))
115    }
116}
117
118impl<M> Configure for Agent<M>
119where
120    M: Configure + SessionManager + Send + Sync,
121{
122    fn configure_endpoint(&self, endpoint: String) {
123        self.session_manager.configure_endpoint(endpoint);
124    }
125    fn configure_labelers_header(&self, labeler_dids: Option<Vec<(Did, bool)>>) {
126        self.session_manager.configure_labelers_header(labeler_dids);
127    }
128    fn configure_proxy_header(&self, did: Did, service_type: impl AsRef<str>) {
129        self.session_manager.configure_proxy_header(did, service_type);
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::inner::Wrapper;
136    use super::utils::{SessionClient, SessionWithEndpointStore};
137    use super::*;
138    use atrium_common::store::Store;
139    use atrium_xrpc::{Error, HttpClient, OutputDataOrBytes, XrpcClient, XrpcRequest};
140    use http::{HeaderMap, HeaderName, HeaderValue, Request, Response, header::CONTENT_TYPE};
141    use serde::{Serialize, de::DeserializeOwned};
142    use std::fmt::Debug;
143    use tokio::sync::Mutex;
144
145    #[derive(Default)]
146    struct RecordData {
147        host: Option<String>,
148        headers: HeaderMap<HeaderValue>,
149    }
150
151    struct MockClient {
152        data: Arc<Mutex<Option<RecordData>>>,
153    }
154
155    impl HttpClient for MockClient {
156        async fn send_http(
157            &self,
158            request: Request<Vec<u8>>,
159        ) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error + Send + Sync + 'static>> {
160            self.data.lock().await.replace(RecordData {
161                host: request.uri().host().map(String::from),
162                headers: request.headers().clone(),
163            });
164            let output = crate::com::atproto::server::get_service_auth::OutputData {
165                token: String::from("fake_token"),
166            };
167            Response::builder()
168                .header(CONTENT_TYPE, "application/json")
169                .body(serde_json::to_vec(&output)?)
170                .map_err(|e| e.into())
171        }
172    }
173
174    impl XrpcClient for MockClient {
175        fn base_uri(&self) -> String {
176            unimplemented!()
177        }
178    }
179
180    #[derive(thiserror::Error, Debug)]
181    enum MockStoreError {}
182
183    struct MockStore;
184
185    impl Store<(), ()> for MockStore {
186        type Error = MockStoreError;
187
188        async fn get(&self, _key: &()) -> Result<Option<()>, Self::Error> {
189            unimplemented!()
190        }
191        async fn set(&self, _key: (), _value: ()) -> Result<(), Self::Error> {
192            unimplemented!()
193        }
194        async fn del(&self, _key: &()) -> Result<(), Self::Error> {
195            unimplemented!()
196        }
197        async fn clear(&self) -> Result<(), Self::Error> {
198            unimplemented!()
199        }
200    }
201
202    impl AuthorizationProvider for MockStore {
203        async fn authorization_token(&self, _: bool) -> Option<AuthorizationToken> {
204            None
205        }
206    }
207
208    struct MockSessionManager {
209        inner: SessionClient<MockStore, MockClient, ()>,
210    }
211
212    impl HttpClient for MockSessionManager {
213        async fn send_http(
214            &self,
215            request: Request<Vec<u8>>,
216        ) -> Result<Response<Vec<u8>>, Box<dyn std::error::Error + Send + Sync + 'static>> {
217            self.inner.send_http(request).await
218        }
219    }
220
221    impl XrpcClient for MockSessionManager {
222        fn base_uri(&self) -> String {
223            self.inner.base_uri()
224        }
225        async fn send_xrpc<P, I, O, E>(
226            &self,
227            request: &XrpcRequest<P, I>,
228        ) -> Result<OutputDataOrBytes<O>, Error<E>>
229        where
230            P: Serialize + Send + Sync,
231            I: Serialize + Send + Sync,
232            O: DeserializeOwned + Send + Sync,
233            E: DeserializeOwned + Send + Sync + Debug,
234        {
235            self.inner.send_xrpc(request).await
236        }
237    }
238
239    impl SessionManager for MockSessionManager {
240        async fn did(&self) -> Option<Did> {
241            Did::new(String::from("did:fake:handle.test")).ok()
242        }
243    }
244
245    impl Configure for MockSessionManager {
246        fn configure_endpoint(&self, endpoint: String) {
247            self.inner.configure_endpoint(endpoint);
248        }
249        fn configure_labelers_header(&self, labeler_dids: Option<Vec<(Did, bool)>>) {
250            self.inner.configure_labelers_header(labeler_dids);
251        }
252        fn configure_proxy_header(&self, did: Did, service_type: impl AsRef<str>) {
253            self.inner.configure_proxy_header(did, service_type);
254        }
255    }
256
257    impl CloneWithProxy for MockSessionManager {
258        fn clone_with_proxy(&self, did: Did, service_type: impl AsRef<str>) -> Self {
259            Self { inner: self.inner.clone_with_proxy(did, service_type) }
260        }
261    }
262
263    fn agent(data: Arc<Mutex<Option<RecordData>>>) -> Agent<MockSessionManager> {
264        let inner = SessionClient::new(
265            Arc::new(SessionWithEndpointStore::new(
266                MockStore {},
267                String::from("https://example.com"),
268            )),
269            MockClient { data },
270        );
271        Agent::new(MockSessionManager { inner })
272    }
273
274    async fn call_service(
275        service: &Service<Wrapper<MockSessionManager>>,
276    ) -> Result<(), Error<crate::com::atproto::server::get_service_auth::Error>> {
277        let output = service
278            .com
279            .atproto
280            .server
281            .get_service_auth(
282                crate::com::atproto::server::get_service_auth::ParametersData {
283                    aud: String::from("did:fake:handle.test"),
284                    exp: None,
285                    lxm: None,
286                }
287                .into(),
288            )
289            .await?;
290        assert_eq!(output.token, "fake_token");
291        Ok(())
292    }
293
294    #[tokio::test]
295    async fn test_new() -> Result<(), Box<dyn std::error::Error>> {
296        let agent = agent(Arc::new(Mutex::new(Default::default())));
297        assert_eq!(agent.did().await, Some(Did::new(String::from("did:fake:handle.test"))?));
298        Ok(())
299    }
300
301    #[tokio::test]
302    async fn test_configure_endpoint() -> Result<(), Box<dyn std::error::Error>> {
303        let data = Arc::new(Mutex::new(Default::default()));
304        let agent = agent(data.clone());
305        call_service(&agent.api).await?;
306        assert_eq!(
307            data.lock().await.as_ref().expect("data should be recorded").host.as_deref(),
308            Some("example.com")
309        );
310        agent.configure_endpoint(String::from("https://pds.example.com"));
311        call_service(&agent.api).await?;
312        assert_eq!(
313            data.lock().await.as_ref().expect("data should be recorded").host.as_deref(),
314            Some("pds.example.com")
315        );
316        Ok(())
317    }
318
319    #[tokio::test]
320    async fn test_configure_labelers_header() -> Result<(), Box<dyn std::error::Error>> {
321        let data = Arc::new(Mutex::new(Default::default()));
322        let agent = agent(data.clone());
323        // not configured
324        {
325            call_service(&agent.api).await?;
326            assert_eq!(
327                data.lock().await.as_ref().expect("data should be recorded").headers,
328                HeaderMap::new()
329            );
330        }
331        // configured 1
332        {
333            agent.configure_labelers_header(Some(vec![(
334                Did::new(String::from("did:fake:labeler.test"))?,
335                false,
336            )]));
337            call_service(&agent.api).await?;
338            assert_eq!(
339                data.lock().await.as_ref().expect("data should be recorded").headers,
340                HeaderMap::from_iter([(
341                    HeaderName::from_static("atproto-accept-labelers"),
342                    HeaderValue::from_static("did:fake:labeler.test"),
343                )])
344            );
345        }
346        // configured 2
347        {
348            agent.configure_labelers_header(Some(vec![
349                (Did::new(String::from("did:fake:labeler.test_redact"))?, true),
350                (Did::new(String::from("did:fake:labeler.test"))?, false),
351            ]));
352            call_service(&agent.api).await?;
353            assert_eq!(
354                data.lock().await.as_ref().expect("data should be recorded").headers,
355                HeaderMap::from_iter([(
356                    HeaderName::from_static("atproto-accept-labelers"),
357                    HeaderValue::from_static(
358                        "did:fake:labeler.test_redact;redact, did:fake:labeler.test"
359                    ),
360                )])
361            );
362        }
363        Ok(())
364    }
365
366    #[tokio::test]
367    async fn test_configure_proxy_header() -> Result<(), Box<dyn std::error::Error>> {
368        let data = Arc::new(Mutex::new(Default::default()));
369        let agent = agent(data.clone());
370        // not configured
371        {
372            call_service(&agent.api).await?;
373            assert_eq!(
374                data.lock().await.as_ref().expect("data should be recorded").headers,
375                HeaderMap::new()
376            );
377        }
378        // labeler service
379        {
380            agent.configure_proxy_header(
381                Did::new(String::from("did:fake:service.test"))?,
382                AtprotoServiceType::AtprotoLabeler,
383            );
384            call_service(&agent.api).await?;
385            assert_eq!(
386                data.lock().await.as_ref().expect("data should be recorded").headers,
387                HeaderMap::from_iter([(
388                    HeaderName::from_static("atproto-proxy"),
389                    HeaderValue::from_static("did:fake:service.test#atproto_labeler"),
390                )])
391            );
392        }
393        // custom service
394        {
395            agent.configure_proxy_header(
396                Did::new(String::from("did:fake:service.test"))?,
397                "custom_service",
398            );
399            call_service(&agent.api).await?;
400            assert_eq!(
401                data.lock().await.as_ref().expect("data should be recorded").headers,
402                HeaderMap::from_iter([(
403                    HeaderName::from_static("atproto-proxy"),
404                    HeaderValue::from_static("did:fake:service.test#custom_service"),
405                )])
406            );
407        }
408        // api_with_proxy
409        {
410            call_service(
411                &agent.api_with_proxy(
412                    Did::new(String::from("did:fake:service.test"))?,
413                    "temp_service",
414                ),
415            )
416            .await?;
417            assert_eq!(
418                data.lock().await.as_ref().expect("data should be recorded").headers,
419                HeaderMap::from_iter([(
420                    HeaderName::from_static("atproto-proxy"),
421                    HeaderValue::from_static("did:fake:service.test#temp_service"),
422                )])
423            );
424            call_service(&agent.api).await?;
425            assert_eq!(
426                data.lock().await.as_ref().expect("data should be recorded").headers,
427                HeaderMap::from_iter([(
428                    HeaderName::from_static("atproto-proxy"),
429                    HeaderValue::from_static("did:fake:service.test#custom_service"),
430                )])
431            );
432        }
433        Ok(())
434    }
435}