tangled
alpha
login
or
join now
alephcubed.com
/
jacquard
forked from
nonbinary.computer/jacquard
0
fork
atom
A better Rust ATProto crate
0
fork
atom
overview
issues
pulls
pipelines
further cleanup
Orual
5 months ago
278eb8f2
3a7692c4
+117
-397
8 changed files
expand all
collapse all
unified
split
crates
jacquard
src
client
at_client.rs
token.rs
client.rs
main.rs
jacquard-common
src
session.rs
jacquard-oauth
src
authstore.rs
client.rs
resolver.rs
-15
crates/jacquard-common/src/session.rs
···
12
12
use std::path::{Path, PathBuf};
13
13
use std::sync::Arc;
14
14
use tokio::sync::RwLock;
15
15
-
use url::Url;
16
16
-
17
17
-
use crate::AuthorizationToken;
18
18
-
use crate::types::did::Did;
19
19
-
20
20
-
#[async_trait::async_trait]
21
21
-
pub trait Session {
22
22
-
async fn did(&self) -> Did<'_>;
23
23
-
24
24
-
async fn endpoint(&self) -> Url;
25
25
-
26
26
-
async fn access_token(&self) -> Result<AuthorizationToken, SessionStoreError>;
27
27
-
28
28
-
async fn refresh(&self) -> Result<AuthorizationToken, SessionStoreError>;
29
29
-
}
30
15
31
16
/// Errors emitted by session stores.
32
17
#[derive(Debug, thiserror::Error, Diagnostic)]
+71
-2
crates/jacquard-oauth/src/authstore.rs
···
1
1
use std::sync::Arc;
2
2
3
3
+
use dashmap::DashMap;
3
4
use jacquard_common::{
4
5
IntoStatic,
5
5
-
session::{FileTokenStore, SessionStore, SessionStoreError},
6
6
+
session::{SessionStore, SessionStoreError},
6
7
types::did::Did,
7
8
};
8
8
-
use smol_str::SmolStr;
9
9
+
use smol_str::{SmolStr, ToSmolStr, format_smolstr};
9
10
10
11
use crate::session::{AuthRequestData, ClientSessionData};
11
12
···
37
38
) -> Result<(), SessionStoreError>;
38
39
39
40
async fn delete_auth_req_info(&self, state: &str) -> Result<(), SessionStoreError>;
41
41
+
}
42
42
+
43
43
+
pub struct MemoryAuthStore {
44
44
+
sessions: DashMap<SmolStr, ClientSessionData<'static>>,
45
45
+
auth_reqs: DashMap<SmolStr, AuthRequestData<'static>>,
46
46
+
}
47
47
+
48
48
+
impl MemoryAuthStore {
49
49
+
pub fn new() -> Self {
50
50
+
Self {
51
51
+
sessions: DashMap::new(),
52
52
+
auth_reqs: DashMap::new(),
53
53
+
}
54
54
+
}
55
55
+
}
56
56
+
57
57
+
#[async_trait::async_trait]
58
58
+
impl ClientAuthStore for MemoryAuthStore {
59
59
+
async fn get_session(
60
60
+
&self,
61
61
+
did: &Did<'_>,
62
62
+
session_id: &str,
63
63
+
) -> Result<Option<ClientSessionData<'_>>, SessionStoreError> {
64
64
+
let key = format_smolstr!("{}_{}", did, session_id);
65
65
+
Ok(self.sessions.get(&key).map(|v| v.clone()))
66
66
+
}
67
67
+
68
68
+
async fn upsert_session(
69
69
+
&self,
70
70
+
session: ClientSessionData<'_>,
71
71
+
) -> Result<(), SessionStoreError> {
72
72
+
let key = format_smolstr!("{}_{}", session.account_did, session.session_id);
73
73
+
self.sessions.insert(key, session.into_static());
74
74
+
Ok(())
75
75
+
}
76
76
+
77
77
+
async fn delete_session(
78
78
+
&self,
79
79
+
did: &Did<'_>,
80
80
+
session_id: &str,
81
81
+
) -> Result<(), SessionStoreError> {
82
82
+
let key = format_smolstr!("{}_{}", did, session_id);
83
83
+
self.sessions.remove(&key);
84
84
+
Ok(())
85
85
+
}
86
86
+
87
87
+
async fn get_auth_req_info(
88
88
+
&self,
89
89
+
state: &str,
90
90
+
) -> Result<Option<AuthRequestData<'_>>, SessionStoreError> {
91
91
+
Ok(self.auth_reqs.get(state).map(|v| v.clone()))
92
92
+
}
93
93
+
94
94
+
async fn save_auth_req_info(
95
95
+
&self,
96
96
+
auth_req_info: &AuthRequestData<'_>,
97
97
+
) -> Result<(), SessionStoreError> {
98
98
+
self.auth_reqs.insert(
99
99
+
auth_req_info.state.clone().to_smolstr(),
100
100
+
auth_req_info.clone().into_static(),
101
101
+
);
102
102
+
Ok(())
103
103
+
}
104
104
+
105
105
+
async fn delete_auth_req_info(&self, state: &str) -> Result<(), SessionStoreError> {
106
106
+
self.auth_reqs.remove(state);
107
107
+
Ok(())
108
108
+
}
40
109
}
41
110
42
111
#[async_trait::async_trait]
+8
crates/jacquard-oauth/src/client.rs
···
18
18
xrpc::{CallOptions, Response, XrpcClient, XrpcExt, XrpcRequest},
19
19
},
20
20
};
21
21
+
use jacquard_identity::JacquardResolver;
21
22
use jose_jwk::JwkSet;
22
23
use std::sync::Arc;
23
24
use tokio::sync::RwLock;
···
30
31
{
31
32
pub registry: Arc<SessionRegistry<T, S>>,
32
33
pub client: Arc<T>,
34
34
+
}
35
35
+
36
36
+
impl<S: ClientAuthStore> OAuthClient<JacquardResolver, S> {
37
37
+
pub fn new(store: S, client_data: ClientData<'static>) -> Self {
38
38
+
let client = JacquardResolver::default();
39
39
+
Self::new_from_resolver(store, client, client_data)
40
40
+
}
33
41
}
34
42
35
43
impl<T, S> OAuthClient<T, S>
+3
crates/jacquard-oauth/src/resolver.rs
···
225
225
Err(ResolverError::HttpStatus(res.status()))
226
226
}
227
227
}
228
228
+
229
229
+
#[async_trait::async_trait]
230
230
+
impl OAuthResolver for jacquard_identity::JacquardResolver {}
+3
-60
crates/jacquard/src/client.rs
···
3
3
//! This module provides HTTP and XRPC client traits along with an authenticated
4
4
//! client implementation that manages session tokens.
5
5
6
6
-
mod at_client;
7
6
pub mod credential_session;
8
8
-
mod token;
9
9
-
10
10
-
pub use at_client::{AtClient, SendOverrides};
7
7
+
pub mod token;
11
8
12
9
pub use jacquard_common::error::{ClientError, XrpcResult};
13
10
pub use jacquard_common::session::{MemorySessionStore, SessionStore, SessionStoreError};
14
11
use jacquard_common::{
15
12
CowStr, IntoStatic,
16
16
-
types::{
17
17
-
string::{Did, Handle},
18
18
-
xrpc::{Response, XrpcRequest},
19
19
-
},
13
13
+
types::string::{Did, Handle},
20
14
};
21
15
pub use token::FileAuthStore;
22
22
-
use url::Url;
23
16
24
17
pub(crate) const NSID_REFRESH_SESSION: &str = "com.atproto.server.refreshSession";
25
18
26
19
/// Basic client wrapper: reqwest transport + in-memory session store.
27
27
-
pub struct BasicClient(AtClient<reqwest::Client, MemorySessionStore<Did<'static>, AuthSession>>);
28
28
-
29
29
-
impl BasicClient {
30
30
-
/// Construct a basic client with minimal inputs.
31
31
-
pub fn new(base: Url) -> Self {
32
32
-
Self(AtClient::new(
33
33
-
reqwest::Client::new(),
34
34
-
base,
35
35
-
MemorySessionStore::default(),
36
36
-
))
37
37
-
}
38
38
-
39
39
-
/// Access the inner stateful client.
40
40
-
pub fn inner(
41
41
-
&self,
42
42
-
) -> &AtClient<reqwest::Client, MemorySessionStore<Did<'static>, AuthSession>> {
43
43
-
&self.0
44
44
-
}
45
45
-
46
46
-
/// Send an XRPC request.
47
47
-
pub async fn send<R: XrpcRequest + Send>(&self, req: R) -> XrpcResult<Response<R>> {
48
48
-
self.0.send(req).await
49
49
-
}
50
50
-
51
51
-
/// Send with per-call overrides.
52
52
-
pub async fn send_with<R: XrpcRequest + Send>(
53
53
-
&self,
54
54
-
req: R,
55
55
-
overrides: SendOverrides<'_>,
56
56
-
) -> XrpcResult<Response<R>> {
57
57
-
self.0.send_with(req, overrides).await
58
58
-
}
59
59
-
60
60
-
/// Get current session.
61
61
-
pub async fn session(&self, did: &Did<'static>) -> Option<AuthSession> {
62
62
-
self.0.session(did).await
63
63
-
}
64
64
-
65
65
-
/// Set the session.
66
66
-
pub async fn set_session(
67
67
-
&self,
68
68
-
session: AuthSession,
69
69
-
) -> core::result::Result<(), SessionStoreError> {
70
70
-
self.0.set_session(session).await
71
71
-
}
72
72
-
73
73
-
/// Base URL of this client.
74
74
-
pub fn base(&self) -> &Url {
75
75
-
self.0.base()
76
76
-
}
77
77
-
}
20
20
+
pub struct BasicClient(); //AtClient<reqwest::Client, MemorySessionStore<Did<'static>, AuthSession>>);
78
21
79
22
/// App password session information from `com.atproto.server.createSession`
80
23
///
-284
crates/jacquard/src/client/at_client.rs
···
1
1
-
use bytes::Bytes;
2
2
-
use jacquard_common::{
3
3
-
AuthorizationToken, IntoStatic,
4
4
-
error::{AuthError, ClientError, HttpError, TransportError, XrpcResult},
5
5
-
http_client::HttpClient,
6
6
-
session::{SessionStore, SessionStoreError},
7
7
-
types::{
8
8
-
did::Did,
9
9
-
xrpc::{CallOptions, Response, XrpcExt, XrpcRequest, build_http_request},
10
10
-
},
11
11
-
};
12
12
-
use url::Url;
13
13
-
14
14
-
use crate::client::{AtpSession, AuthSession, NSID_REFRESH_SESSION};
15
15
-
16
16
-
/// Per-call overrides when sending via `AtClient`.
17
17
-
#[derive(Debug, Clone)]
18
18
-
pub struct SendOverrides<'a> {
19
19
-
/// Optional DID override for this call.
20
20
-
pub did: Option<Did<'a>>,
21
21
-
/// Optional base URI override for this call.
22
22
-
pub base_uri: Option<Url>,
23
23
-
/// Per-request options such as auth, proxy, labelers, extra headers.
24
24
-
pub options: CallOptions<'a>,
25
25
-
/// Whether to auto-refresh on expired/invalid token and retry once.
26
26
-
pub auto_refresh: bool,
27
27
-
}
28
28
-
29
29
-
impl Default for SendOverrides<'_> {
30
30
-
fn default() -> Self {
31
31
-
Self {
32
32
-
did: None,
33
33
-
base_uri: None,
34
34
-
options: CallOptions::default(),
35
35
-
auto_refresh: true,
36
36
-
}
37
37
-
}
38
38
-
}
39
39
-
40
40
-
impl<'a> SendOverrides<'a> {
41
41
-
/// Construct default overrides (no base override, auto-refresh enabled).
42
42
-
pub fn new() -> Self {
43
43
-
Self {
44
44
-
did: None,
45
45
-
base_uri: None,
46
46
-
options: CallOptions::default(),
47
47
-
auto_refresh: true,
48
48
-
}
49
49
-
}
50
50
-
/// Override the base URI for this call only.
51
51
-
pub fn base_uri(mut self, base: Url) -> Self {
52
52
-
self.base_uri = Some(base);
53
53
-
self
54
54
-
}
55
55
-
/// Provide a full set of call options (auth/headers/etc.).
56
56
-
pub fn options(mut self, opts: CallOptions<'a>) -> Self {
57
57
-
self.options = opts;
58
58
-
self
59
59
-
}
60
60
-
61
61
-
/// Provide a full set of call options (auth/headers/etc.).
62
62
-
pub fn did(mut self, did: Did<'a>) -> Self {
63
63
-
self.did = Some(did);
64
64
-
self
65
65
-
}
66
66
-
/// Enable or disable one-shot auto-refresh + retry behavior.
67
67
-
pub fn auto_refresh(mut self, enable: bool) -> Self {
68
68
-
self.auto_refresh = enable;
69
69
-
self
70
70
-
}
71
71
-
}
72
72
-
73
73
-
/// Stateful client for AT Protocol XRPC with token storage and auto-refresh.
74
74
-
///
75
75
-
/// Example (file-backed tokens)
76
76
-
/// ```ignore
77
77
-
/// use jacquard::client::{AtClient, FileTokenStore, TokenStore};
78
78
-
/// use jacquard::api::com_atproto::server::create_session::CreateSession;
79
79
-
/// use jacquard::client::AtClient as _; // method resolution
80
80
-
/// use jacquard::CowStr;
81
81
-
///
82
82
-
/// #[tokio::main]
83
83
-
/// async fn main() -> miette::Result<()> {
84
84
-
/// let base = url::Url::parse("https://bsky.social")?;
85
85
-
/// let store = FileTokenStore::new("/tmp/jacquard-session.json");
86
86
-
/// let client = AtClient::new(reqwest::Client::new(), base, store);
87
87
-
/// let session = client
88
88
-
/// .send(
89
89
-
/// CreateSession::new()
90
90
-
/// .identifier(CowStr::from("alice.example"))
91
91
-
/// .password(CowStr::from("app-password"))
92
92
-
/// .build(),
93
93
-
/// )
94
94
-
/// .await?
95
95
-
/// .into_output()?;
96
96
-
/// client.set_session(session.into()).await?;
97
97
-
/// Ok(())
98
98
-
/// }
99
99
-
/// ```
100
100
-
pub struct AtClient<C: HttpClient, S> {
101
101
-
transport: C,
102
102
-
base: Url,
103
103
-
tokens: S,
104
104
-
refresh_lock: tokio::sync::Mutex<Option<Did<'static>>>,
105
105
-
}
106
106
-
107
107
-
impl<C: HttpClient, S: SessionStore<Did<'static>, AuthSession>> AtClient<C, S> {
108
108
-
/// Create a new client with a transport, base URL, and token store.
109
109
-
pub fn new(transport: C, base: Url, tokens: S) -> Self {
110
110
-
Self {
111
111
-
transport,
112
112
-
base,
113
113
-
tokens,
114
114
-
refresh_lock: tokio::sync::Mutex::new(None),
115
115
-
}
116
116
-
}
117
117
-
118
118
-
/// Get the base URL of this client.
119
119
-
pub fn base(&self) -> &Url {
120
120
-
&self.base
121
121
-
}
122
122
-
123
123
-
/// Access the underlying transport.
124
124
-
pub fn transport(&self) -> &C {
125
125
-
&self.transport
126
126
-
}
127
127
-
128
128
-
/// Get the current session, if any.
129
129
-
pub async fn session(&self, did: &Did<'static>) -> Option<AuthSession> {
130
130
-
self.tokens.get(did).await
131
131
-
}
132
132
-
133
133
-
/// Set the current session in the token store.
134
134
-
pub async fn set_session(&self, session: AuthSession) -> Result<(), SessionStoreError> {
135
135
-
let s = session.clone();
136
136
-
let did = s.did().clone().into_static();
137
137
-
self.refresh_lock.lock().await.replace(did.clone());
138
138
-
self.tokens.set(did, session).await
139
139
-
}
140
140
-
141
141
-
/// Send an XRPC request using the client's base URL and default behavior.
142
142
-
pub async fn send<R: XrpcRequest + Send>(&self, req: R) -> XrpcResult<Response<R>> {
143
143
-
self.send_with(req, SendOverrides::new()).await
144
144
-
}
145
145
-
146
146
-
/// Send an XRPC request with per-call overrides.
147
147
-
pub async fn send_with<R: XrpcRequest + Send>(
148
148
-
&self,
149
149
-
req: R,
150
150
-
mut overrides: SendOverrides<'_>,
151
151
-
) -> XrpcResult<Response<R>> {
152
152
-
let base = overrides
153
153
-
.base_uri
154
154
-
.clone()
155
155
-
.unwrap_or_else(|| self.base.clone());
156
156
-
let is_refresh = R::NSID == NSID_REFRESH_SESSION;
157
157
-
158
158
-
let mut current_did = None;
159
159
-
if overrides.options.auth.is_none() {
160
160
-
if let Ok(guard) = self.refresh_lock.try_lock() {
161
161
-
if let Some(ref did) = *guard {
162
162
-
current_did = Some(did.clone());
163
163
-
if let Some(s) = self.tokens.get(&did).await {
164
164
-
overrides.options.auth = Some(
165
165
-
if let Some(refresh_tok) = s.refresh_token()
166
166
-
&& is_refresh
167
167
-
{
168
168
-
AuthorizationToken::Bearer(refresh_tok.clone().into_static())
169
169
-
} else {
170
170
-
AuthorizationToken::Bearer(s.access_token().clone().into_static())
171
171
-
},
172
172
-
);
173
173
-
}
174
174
-
}
175
175
-
}
176
176
-
}
177
177
-
178
178
-
let http_request =
179
179
-
build_http_request(&base, &req, &overrides.options).map_err(TransportError::from)?;
180
180
-
let http_response = self
181
181
-
.transport
182
182
-
.send_http(http_request)
183
183
-
.await
184
184
-
.map_err(|e| TransportError::Other(Box::new(e)))?;
185
185
-
let status = http_response.status();
186
186
-
let buffer = Bytes::from(http_response.into_body());
187
187
-
188
188
-
if !status.is_success() && !matches!(status.as_u16(), 400 | 401) {
189
189
-
return Err(HttpError {
190
190
-
status,
191
191
-
body: Some(buffer),
192
192
-
}
193
193
-
.into());
194
194
-
}
195
195
-
196
196
-
if overrides.auto_refresh
197
197
-
&& !is_refresh
198
198
-
&& overrides.options.auth.is_some()
199
199
-
&& Self::is_auth_expired(status, &buffer)
200
200
-
{
201
201
-
self.refresh_once().await?;
202
202
-
203
203
-
let mut retry_opts = overrides.options.clone();
204
204
-
if let Some(curr_did) = current_did {
205
205
-
if let Some(s) = self.tokens.get(&curr_did).await {
206
206
-
retry_opts.auth = Some(AuthorizationToken::Bearer(
207
207
-
s.access_token().clone().into_static(),
208
208
-
));
209
209
-
}
210
210
-
}
211
211
-
let http_request =
212
212
-
build_http_request(&base, &req, &retry_opts).map_err(TransportError::from)?;
213
213
-
let http_response = self
214
214
-
.transport
215
215
-
.send_http(http_request)
216
216
-
.await
217
217
-
.map_err(|e| TransportError::Other(Box::new(e)))?;
218
218
-
let status = http_response.status();
219
219
-
let buffer = Bytes::from(http_response.into_body());
220
220
-
221
221
-
if !status.is_success() && !matches!(status.as_u16(), 400 | 401) {
222
222
-
return Err(HttpError {
223
223
-
status,
224
224
-
body: Some(buffer),
225
225
-
}
226
226
-
.into());
227
227
-
}
228
228
-
return Ok(Response::new(buffer, status));
229
229
-
}
230
230
-
231
231
-
Ok(Response::new(buffer, status))
232
232
-
}
233
233
-
234
234
-
async fn refresh_once(&self) -> XrpcResult<()> {
235
235
-
let guard = self.refresh_lock.lock().await;
236
236
-
if let Some(ref did) = *guard {
237
237
-
if let Some(s) = self.tokens.get(did).await {
238
238
-
if let Some(refresh_tok) = s.refresh_token() {
239
239
-
let refresh_resp = self
240
240
-
.transport
241
241
-
.xrpc(self.base.clone())
242
242
-
.auth(AuthorizationToken::Bearer(
243
243
-
refresh_tok.clone().into_static(),
244
244
-
))
245
245
-
.send(&jacquard_api::com_atproto::server::refresh_session::RefreshSession)
246
246
-
.await?;
247
247
-
let refreshed = match refresh_resp.into_output() {
248
248
-
Ok(o) => AtpSession::from(o),
249
249
-
Err(_) => return Err(ClientError::Auth(AuthError::RefreshFailed)),
250
250
-
};
251
251
-
252
252
-
let mut session = s.clone();
253
253
-
session.set_access_token(refreshed.access_jwt);
254
254
-
session.set_refresh_token(refreshed.refresh_jwt);
255
255
-
256
256
-
self.set_session(session)
257
257
-
.await
258
258
-
.map_err(|_| ClientError::Auth(AuthError::RefreshFailed))?;
259
259
-
Ok(())
260
260
-
} else {
261
261
-
Err(ClientError::Auth(AuthError::RefreshFailed))
262
262
-
}
263
263
-
} else {
264
264
-
Err(ClientError::Auth(AuthError::NotAuthenticated))
265
265
-
}
266
266
-
} else {
267
267
-
Err(ClientError::Auth(AuthError::NotAuthenticated))
268
268
-
}
269
269
-
}
270
270
-
271
271
-
fn is_auth_expired(status: http::StatusCode, buffer: &Bytes) -> bool {
272
272
-
if status.as_u16() == 401 {
273
273
-
return true;
274
274
-
}
275
275
-
if status.as_u16() == 400 {
276
276
-
if let Ok(val) = serde_json::from_slice::<serde_json::Value>(buffer) {
277
277
-
if let Some(code) = val.get("error").and_then(|v| v.as_str()) {
278
278
-
return matches!(code, "ExpiredToken" | "InvalidToken");
279
279
-
}
280
280
-
}
281
281
-
}
282
282
-
false
283
283
-
}
284
284
-
}
+3
-7
crates/jacquard/src/client/token.rs
···
1
1
use jacquard_common::IntoStatic;
2
2
use jacquard_common::cowstr::ToCowStr;
3
3
use jacquard_common::session::{FileTokenStore, SessionStore, SessionStoreError};
4
4
-
use jacquard_common::types::string::{Datetime, Did, Handle};
4
4
+
use jacquard_common::types::string::{Datetime, Did};
5
5
use jacquard_oauth::scopes::Scope;
6
6
use jacquard_oauth::session::{AuthRequestData, ClientSessionData, DpopClientData, DpopReqData};
7
7
use jacquard_oauth::types::OAuthTokenType;
8
8
use jose_jwk::Key;
9
9
-
use serde::de::DeserializeOwned;
10
9
use serde::{Deserialize, Serialize};
11
10
use serde_json::Value;
12
12
-
use std::fmt::Display;
13
13
-
use std::hash::Hash;
14
14
-
use std::path::{Path, PathBuf};
15
11
use url::Url;
16
12
17
13
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
···
22
18
}
23
19
24
20
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
25
25
-
struct StoredAtSession {
21
21
+
pub struct StoredAtSession {
26
22
access_jwt: String,
27
23
refresh_jwt: String,
28
24
did: String,
···
32
28
}
33
29
34
30
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
35
35
-
struct OAuthSession {
31
31
+
pub struct OAuthSession {
36
32
account_did: String,
37
33
session_id: String,
38
34
+29
-29
crates/jacquard/src/main.rs
···
31
31
let resolver = slingshot_resolver_default();
32
32
let handle = Handle::new(args.username.as_ref()).into_diagnostic()?;
33
33
let (_did, pds_url) = resolver.pds_for_handle(&handle).await.into_diagnostic()?;
34
34
-
let client = BasicClient::new(pds_url);
34
34
+
// let client = BasicClient::new(pds_url);
35
35
36
36
-
// Create session
37
37
-
let session = AtpSession::from(
38
38
-
client
39
39
-
.send(
40
40
-
CreateSession::new()
41
41
-
.identifier(args.username)
42
42
-
.password(args.password)
43
43
-
.build(),
44
44
-
)
45
45
-
.await?
46
46
-
.into_output()?,
47
47
-
);
36
36
+
// // Create session
37
37
+
// let session = AtpSession::from(
38
38
+
// client
39
39
+
// .send(
40
40
+
// CreateSession::new()
41
41
+
// .identifier(args.username)
42
42
+
// .password(args.password)
43
43
+
// .build(),
44
44
+
// )
45
45
+
// .await?
46
46
+
// .into_output()?,
47
47
+
// );
48
48
49
49
-
println!("logged in as {} ({})", session.handle, session.did);
50
50
-
client.set_session(session.into()).await.into_diagnostic()?;
49
49
+
// println!("logged in as {} ({})", session.handle, session.did);
50
50
+
// client.set_session(session.into()).await.into_diagnostic()?;
51
51
52
52
-
// Fetch timeline
53
53
-
println!("\nfetching timeline...");
54
54
-
let timeline = client
55
55
-
.send(GetTimeline::new().limit(5).build())
56
56
-
.await?
57
57
-
.into_output()?;
52
52
+
// // Fetch timeline
53
53
+
// println!("\nfetching timeline...");
54
54
+
// let timeline = client
55
55
+
// .send(GetTimeline::new().limit(5).build())
56
56
+
// .await?
57
57
+
// .into_output()?;
58
58
59
59
-
println!("\ntimeline ({} posts):", timeline.feed.len());
60
60
-
for (i, post) in timeline.feed.iter().enumerate() {
61
61
-
println!("\n{}. by {}", i + 1, post.post.author.handle);
62
62
-
println!(
63
63
-
" {}",
64
64
-
serde_json::to_string_pretty(&post.post.record).into_diagnostic()?
65
65
-
);
66
66
-
}
59
59
+
// println!("\ntimeline ({} posts):", timeline.feed.len());
60
60
+
// for (i, post) in timeline.feed.iter().enumerate() {
61
61
+
// println!("\n{}. by {}", i + 1, post.post.author.handle);
62
62
+
// println!(
63
63
+
// " {}",
64
64
+
// serde_json::to_string_pretty(&post.post.record).into_diagnostic()?
65
65
+
// );
66
66
+
// }
67
67
68
68
Ok(())
69
69
}