fork to do stuff
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 159 lines 4.5 kB view raw
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}