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