fork to do stuff
1use anyhow::{Context, Result};
2use chrono::Utc;
3use reqwest::{
4 header::{HeaderMap, HeaderValue, AUTHORIZATION},
5 Client as HttpClient,
6};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9
10use crate::credentials::Credentials;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct Session {
14 #[serde(rename = "accessJwt")]
15 pub access_jwt: String,
16 #[serde(rename = "refreshJwt")]
17 pub refresh_jwt: String,
18 pub handle: String,
19 pub did: String,
20}
21
22#[derive(Debug, Clone)]
23pub struct AtProtoClient {
24 http_client: HttpClient,
25 base_url: String,
26 session: Option<Session>,
27}
28
29#[derive(Debug, Serialize)]
30struct LoginRequest {
31 identifier: String,
32 password: String,
33}
34
35#[derive(Debug, Serialize)]
36struct CreateRecordRequest {
37 repo: String,
38 collection: String,
39 record: Value,
40}
41
42#[derive(Debug, Serialize)]
43struct BlipRecord {
44 #[serde(rename = "$type")]
45 record_type: String,
46 content: String,
47 #[serde(rename = "createdAt")]
48 created_at: String,
49}
50
51#[derive(Debug, Deserialize)]
52struct CreateRecordResponse {
53 uri: String,
54 cid: String,
55}
56
57impl AtProtoClient {
58 pub fn new(pds_uri: &str) -> Self {
59 Self {
60 http_client: HttpClient::new(),
61 base_url: pds_uri.to_string(),
62 session: None,
63 }
64 }
65
66 pub async fn login(&mut self, credentials: &Credentials) -> Result<()> {
67 let login_url = format!("{}/xrpc/com.atproto.server.createSession", self.base_url);
68
69 let request = LoginRequest {
70 identifier: credentials.username.clone(),
71 password: credentials.password.clone(),
72 };
73
74 let response = self
75 .http_client
76 .post(&login_url)
77 .header("Content-Type", "application/json")
78 .json(&request)
79 .send()
80 .await
81 .context("Failed to send login request")?;
82
83 if !response.status().is_success() {
84 let status = response.status();
85 let error_text = response.text().await.unwrap_or_default();
86 anyhow::bail!("Login failed: {} - {}", status, error_text);
87 }
88
89 let session: Session = response
90 .json()
91 .await
92 .context("Failed to parse login response")?;
93
94 println!("Authenticated as: {}", session.handle);
95 self.session = Some(session);
96 Ok(())
97 }
98
99 pub async fn publish_blip(&self, content: &str) -> Result<String> {
100 let session = self
101 .session
102 .as_ref()
103 .context("Not authenticated. Please run 'thought login' first.")?;
104
105 let timestamp = Utc::now().to_rfc3339().replace("+00:00", "Z");
106
107 let record = BlipRecord {
108 record_type: "stream.thought.blip".to_string(),
109 content: content.to_string(),
110 created_at: timestamp.clone(),
111 };
112
113 let request = CreateRecordRequest {
114 repo: session.did.clone(),
115 collection: "stream.thought.blip".to_string(),
116 record: serde_json::to_value(&record).context("Failed to serialize blip record")?,
117 };
118
119 let create_url = format!("{}/xrpc/com.atproto.repo.createRecord", self.base_url);
120
121 let mut headers = HeaderMap::new();
122 headers.insert(
123 AUTHORIZATION,
124 HeaderValue::from_str(&format!("Bearer {}", session.access_jwt))
125 .context("Invalid authorization header")?,
126 );
127 headers.insert("Content-Type", HeaderValue::from_static("application/json"));
128
129 let response = self
130 .http_client
131 .post(&create_url)
132 .headers(headers)
133 .json(&request)
134 .send()
135 .await
136 .context("Failed to send create record request")?;
137
138 if !response.status().is_success() {
139 let status = response.status();
140 let error_text = response.text().await.unwrap_or_default();
141 anyhow::bail!("Failed to publish blip: {} - {}", status, error_text);
142 }
143
144 let create_response: CreateRecordResponse = response
145 .json()
146 .await
147 .context("Failed to parse create record response")?;
148
149 Ok(timestamp)
150 }
151
152 pub fn is_authenticated(&self) -> bool {
153 self.session.is_some()
154 }
155
156 pub fn get_user_did(&self) -> Option<String> {
157 self.session.as_ref().map(|s| s.did.clone())
158 }
159}