···000001pub mod embed;
2pub mod feed;
3-pub mod richtext;
···1+// @generated by jacquard-lexicon. DO NOT EDIT.
2+//
3+// This file was automatically generated from Lexicon schemas.
4+// Any manual changes will be overwritten on the next regeneration.
5+6pub mod embed;
7pub mod feed;
8+pub mod richtext;
···000001pub mod external;
2pub mod images;
3pub mod record;
4pub mod record_with_media;
5-pub mod video;
···1+// @generated by jacquard-lexicon. DO NOT EDIT.
2+//
3+// This file was automatically generated from Lexicon schemas.
4+// Any manual changes will be overwritten on the next regeneration.
5+6pub mod external;
7pub mod images;
8pub mod record;
9pub mod record_with_media;
10+pub mod video;
···1+// @generated by jacquard-lexicon. DO NOT EDIT.
2+//
3+// This file was automatically generated from Lexicon schemas.
4+// Any manual changes will be overwritten on the next regeneration.
5+6pub mod get_author_feed;
7+pub mod post;
···1+// @generated by jacquard-lexicon. DO NOT EDIT.
2+//
3+// This file was automatically generated from Lexicon schemas.
4+// Any manual changes will be overwritten on the next regeneration.
5+6+pub mod facet;
···00000001///Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.
02#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
3#[serde(rename_all = "camelCase")]
4pub struct ByteSlice<'a> {
5 pub byte_end: i64,
6 pub byte_start: i64,
7}
0000000000008///Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.
09#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
10#[serde(rename_all = "camelCase")]
11pub struct Link<'a> {
012 pub uri: jacquard_common::types::string::Uri<'a>,
13}
0000000000014///Annotation of a sub-string within rich text.
015#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
16#[serde(rename_all = "camelCase")]
17pub struct Facet<'a> {
018 pub features: Vec<jacquard_common::types::value::Data<'a>>,
19- pub index: jacquard_common::types::value::Data<'a>,
00000000000020}
021///Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.
022#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
23#[serde(rename_all = "camelCase")]
24pub struct Mention<'a> {
025 pub did: jacquard_common::types::string::Did<'a>,
26}
0000000000027///Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').
028#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
29#[serde(rename_all = "camelCase")]
30pub struct Tag<'a> {
031 pub tag: jacquard_common::CowStr<'a>,
32}
0000000000
···1+// @generated by jacquard-lexicon. DO NOT EDIT.
2+//
3+// Lexicon: app.bsky.richtext.facet
4+//
5+// This file was automatically generated from Lexicon schemas.
6+// Any manual changes will be overwritten on the next regeneration.
7+8///Specifies the sub-string range a facet feature applies to. Start index is inclusive, end index is exclusive. Indices are zero-indexed, counting bytes of the UTF-8 encoded text. NOTE: some languages, like Javascript, use UTF-16 or Unicode codepoints for string slice indexing; in these languages, convert to byte arrays before working with facets.
9+#[jacquard_derive::lexicon]
10#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
11#[serde(rename_all = "camelCase")]
12pub struct ByteSlice<'a> {
13 pub byte_end: i64,
14 pub byte_start: i64,
15}
16+17+impl jacquard_common::IntoStatic for ByteSlice<'_> {
18+ type Output = ByteSlice<'static>;
19+ fn into_static(self) -> Self::Output {
20+ ByteSlice {
21+ byte_end: self.byte_end.into_static(),
22+ byte_start: self.byte_start.into_static(),
23+ extra_data: self.extra_data.into_static(),
24+ }
25+ }
26+}
27+28///Facet feature for a URL. The text URL may have been simplified or truncated, but the facet reference should be a complete URL.
29+#[jacquard_derive::lexicon]
30#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
31#[serde(rename_all = "camelCase")]
32pub struct Link<'a> {
33+ #[serde(borrow)]
34 pub uri: jacquard_common::types::string::Uri<'a>,
35}
36+37+impl jacquard_common::IntoStatic for Link<'_> {
38+ type Output = Link<'static>;
39+ fn into_static(self) -> Self::Output {
40+ Link {
41+ uri: self.uri.into_static(),
42+ extra_data: self.extra_data.into_static(),
43+ }
44+ }
45+}
46+47///Annotation of a sub-string within rich text.
48+#[jacquard_derive::lexicon]
49#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
50#[serde(rename_all = "camelCase")]
51pub struct Facet<'a> {
52+ #[serde(borrow)]
53 pub features: Vec<jacquard_common::types::value::Data<'a>>,
54+ #[serde(borrow)]
55+ pub index: test_generated::app_bsky::richtext::facet::ByteSlice<'a>,
56+}
57+58+impl jacquard_common::IntoStatic for Facet<'_> {
59+ type Output = Facet<'static>;
60+ fn into_static(self) -> Self::Output {
61+ Facet {
62+ features: self.features.into_static(),
63+ index: self.index.into_static(),
64+ extra_data: self.extra_data.into_static(),
65+ }
66+ }
67}
68+69///Facet feature for mention of another account. The text is usually a handle, including a '@' prefix, but the facet reference is a DID.
70+#[jacquard_derive::lexicon]
71#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
72#[serde(rename_all = "camelCase")]
73pub struct Mention<'a> {
74+ #[serde(borrow)]
75 pub did: jacquard_common::types::string::Did<'a>,
76}
77+78+impl jacquard_common::IntoStatic for Mention<'_> {
79+ type Output = Mention<'static>;
80+ fn into_static(self) -> Self::Output {
81+ Mention {
82+ did: self.did.into_static(),
83+ extra_data: self.extra_data.into_static(),
84+ }
85+ }
86+}
87+88///Facet feature for a hashtag. The text usually includes a '#' prefix, but the facet reference should not (except in the case of 'double hash tags').
89+#[jacquard_derive::lexicon]
90#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
91#[serde(rename_all = "camelCase")]
92pub struct Tag<'a> {
93+ #[serde(borrow)]
94 pub tag: jacquard_common::CowStr<'a>,
95}
96+97+impl jacquard_common::IntoStatic for Tag<'_> {
98+ type Output = Tag<'static>;
99+ fn into_static(self) -> Self::Output {
100+ Tag {
101+ tag: self.tag.into_static(),
102+ extra_data: self.extra_data.into_static(),
103+ }
104+ }
105+}
···1+// @generated by jacquard-lexicon. DO NOT EDIT.
2+//
3+// This file was automatically generated from Lexicon schemas.
4+// Any manual changes will be overwritten on the next regeneration.
5+6pub mod label;
7+pub mod repo;
···1+// @generated by jacquard-lexicon. DO NOT EDIT.
2+//
3+// This file was automatically generated from Lexicon schemas.
4+// Any manual changes will be overwritten on the next regeneration.
5+6+pub mod strong_ref;
···1+// @generated by jacquard-lexicon. DO NOT EDIT.
2+//
3+// This file was automatically generated from Lexicon schemas.
4+// Any manual changes will be overwritten on the next regeneration.
5+6pub mod app_bsky;
7+pub mod com_atproto;
+149-25
crates/jacquard/src/client.rs
···4use std::fmt::Display;
5use std::future::Future;
67-pub use error::{ClientError, Result};
8use bytes::Bytes;
09use http::{
10 HeaderName, HeaderValue, Request,
11 header::{AUTHORIZATION, CONTENT_TYPE, InvalidHeaderValue},
12};
13pub use response::Response;
14-use serde::Serialize;
0000000000000000001516-use jacquard_common::{CowStr, types::xrpc::{XrpcMethod, XrpcRequest}};
000000000000000000000001718pub trait HttpClient {
19 type Error: std::error::Error + Display + Send + Sync + 'static;
···9899 // Add query parameters for Query methods
100 if let XrpcMethod::Query = R::METHOD {
101- if let Ok(qs) = serde_html_form::to_string(&request) {
102- if !qs.is_empty() {
103- uri.push('?');
104- uri.push_str(&qs);
105- }
106 }
107 }
108···139 }
140141 // Serialize body for procedures
142- let body = if let XrpcMethod::Procedure(encoding) = R::METHOD {
143- if encoding == "application/json" {
144- serde_json::to_vec(&request).map_err(error::EncodeError::Json)?
145- } else {
146- // For other encodings, we'd need different serialization
147- vec![]
148- }
149 } else {
150 vec![]
151 };
1520153 let http_request = builder.body(body).expect("Failed to build HTTP request");
154155 // Send HTTP request
156- let http_response = client.send_http(http_request).await.map_err(|e| {
157- error::TransportError::Other(Box::new(e))
158- })?;
0000159160- // Check status
161- if !http_response.status().is_success() {
0162 return Err(ClientError::Http(error::HttpError {
163- status: http_response.status(),
164- body: Some(Bytes::from(http_response.body().clone())),
165 }));
166 }
167168- // Convert to Response
169- let buffer = Bytes::from(http_response.into_body());
170- Ok(Response::new(buffer))
00000000000000000000000000000000000000000000000000000000000000000000000000000000000171}
···4use std::fmt::Display;
5use std::future::Future;
607use bytes::Bytes;
8+pub use error::{ClientError, Result};
9use http::{
10 HeaderName, HeaderValue, Request,
11 header::{AUTHORIZATION, CONTENT_TYPE, InvalidHeaderValue},
12};
13pub use response::Response;
14+15+use jacquard_common::{
16+ CowStr, IntoStatic,
17+ types::{
18+ string::{Did, Handle},
19+ xrpc::{XrpcMethod, XrpcRequest},
20+ },
21+};
22+23+/// Implement HttpClient for reqwest::Client
24+impl HttpClient for reqwest::Client {
25+ type Error = reqwest::Error;
26+27+ async fn send_http(
28+ &self,
29+ request: Request<Vec<u8>>,
30+ ) -> core::result::Result<http::Response<Vec<u8>>, Self::Error> {
31+ // Convert http::Request to reqwest::Request
32+ let (parts, body) = request.into_parts();
3334+ let mut req = self.request(parts.method, parts.uri.to_string()).body(body);
35+36+ // Copy headers
37+ for (name, value) in parts.headers.iter() {
38+ req = req.header(name.as_str(), value.as_bytes());
39+ }
40+41+ // Send request
42+ let resp = req.send().await?;
43+44+ // Convert reqwest::Response to http::Response
45+ let mut builder = http::Response::builder().status(resp.status());
46+47+ // Copy headers
48+ for (name, value) in resp.headers().iter() {
49+ builder = builder.header(name.as_str(), value.as_bytes());
50+ }
51+52+ // Read body
53+ let body = resp.bytes().await?.to_vec();
54+55+ Ok(builder.body(body).expect("Failed to build response"))
56+ }
57+}
5859pub trait HttpClient {
60 type Error: std::error::Error + Display + Send + Sync + 'static;
···139140 // Add query parameters for Query methods
141 if let XrpcMethod::Query = R::METHOD {
142+ let qs = serde_html_form::to_string(&request).map_err(error::EncodeError::from)?;
143+ if !qs.is_empty() {
144+ uri.push('?');
145+ uri.push_str(&qs);
0146 }
147 }
148···179 }
180181 // Serialize body for procedures
182+ let body = if let XrpcMethod::Procedure(_) = R::METHOD {
183+ request.encode_body()?
00000184 } else {
185 vec![]
186 };
187188+ // TODO: make this not panic
189 let http_request = builder.body(body).expect("Failed to build HTTP request");
190191 // Send HTTP request
192+ let http_response = client
193+ .send_http(http_request)
194+ .await
195+ .map_err(|e| error::TransportError::Other(Box::new(e)))?;
196+197+ let status = http_response.status();
198+ let buffer = Bytes::from(http_response.into_body());
199200+ // XRPC errors come as 400/401 with structured error bodies
201+ // Other error status codes (404, 500, etc.) are generic HTTP errors
202+ if !status.is_success() && !matches!(status.as_u16(), 400 | 401) {
203 return Err(ClientError::Http(error::HttpError {
204+ status,
205+ body: Some(buffer),
206 }));
207 }
208209+ // Response will parse XRPC errors for 400/401, or output for 2xx
210+ Ok(Response::new(buffer, status))
211+}
212+213+/// Session information from createSession
214+#[derive(Debug, Clone)]
215+pub struct Session {
216+ pub access_jwt: CowStr<'static>,
217+ pub refresh_jwt: CowStr<'static>,
218+ pub did: Did<'static>,
219+ pub handle: Handle<'static>,
220+}
221+222+impl From<jacquard_api::com_atproto::server::create_session::CreateSessionOutput<'_>> for Session {
223+ fn from(
224+ output: jacquard_api::com_atproto::server::create_session::CreateSessionOutput<'_>,
225+ ) -> Self {
226+ Self {
227+ access_jwt: output.access_jwt.into_static(),
228+ refresh_jwt: output.refresh_jwt.into_static(),
229+ did: output.did.into_static(),
230+ handle: output.handle.into_static(),
231+ }
232+ }
233+}
234+235+/// Authenticated XRPC client that includes session tokens
236+pub struct AuthenticatedClient<C> {
237+ client: C,
238+ base_uri: CowStr<'static>,
239+ session: Option<Session>,
240+}
241+242+impl<C> AuthenticatedClient<C> {
243+ /// Create a new authenticated client with a base URI
244+ pub fn new(client: C, base_uri: CowStr<'static>) -> Self {
245+ Self {
246+ client,
247+ base_uri: base_uri,
248+ session: None,
249+ }
250+ }
251+252+ /// Set the session
253+ pub fn set_session(&mut self, session: Session) {
254+ self.session = Some(session);
255+ }
256+257+ /// Get the current session
258+ pub fn session(&self) -> Option<&Session> {
259+ self.session.as_ref()
260+ }
261+262+ /// Clear the session
263+ pub fn clear_session(&mut self) {
264+ self.session = None;
265+ }
266+}
267+268+impl<C: HttpClient> HttpClient for AuthenticatedClient<C> {
269+ type Error = C::Error;
270+271+ fn send_http(
272+ &self,
273+ request: Request<Vec<u8>>,
274+ ) -> impl Future<Output = core::result::Result<http::Response<Vec<u8>>, Self::Error>> {
275+ self.client.send_http(request)
276+ }
277+}
278+279+impl<C: HttpClient> XrpcClient for AuthenticatedClient<C> {
280+ fn base_uri(&self) -> CowStr<'_> {
281+ self.base_uri.clone()
282+ }
283+284+ async fn authorization_token(&self, is_refresh: bool) -> Option<AuthorizationToken<'_>> {
285+ if is_refresh {
286+ self.session
287+ .as_ref()
288+ .map(|s| AuthorizationToken::Bearer(s.refresh_jwt.clone()))
289+ } else {
290+ self.session
291+ .as_ref()
292+ .map(|s| AuthorizationToken::Bearer(s.access_jwt.clone()))
293+ }
294+ }
295}