adding sync for multiple terminal using global timestamps (copy of #1 because resubmitting did not work) #2

open
opened by rubberducky.guru targeting main from rubberducky.guru/thought-stream-cli: main

Added sync functionality if you have several terminals open. Not much on it's own, but it also makes the timestamp of each messages use the created_at property from the blip record instead of just taking whatever the current time is before displaying it.

Changed files
+24 -21
src
+20 -15
src/client.rs
··· 1 use anyhow::{Context, Result}; 2 use chrono::Utc; 3 - use reqwest::{Client as HttpClient, header::{HeaderMap, HeaderValue, AUTHORIZATION}}; 4 use serde::{Deserialize, Serialize}; 5 use serde_json::Value; 6 ··· 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) ··· 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) ··· 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 { ··· 151 pub fn get_user_did(&self) -> Option<String> { 152 self.session.as_ref().map(|s| s.did.clone()) 153 } 154 - }
··· 1 use anyhow::{Context, Result}; 2 use chrono::Utc; 3 + use reqwest::{ 4 + header::{HeaderMap, HeaderValue, AUTHORIZATION}, 5 + Client as HttpClient, 6 + }; 7 use serde::{Deserialize, Serialize}; 8 use serde_json::Value; 9 ··· 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) ··· 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) ··· 146 .await 147 .context("Failed to parse create record response")?; 148 149 + Ok(timestamp) 150 } 151 152 pub fn is_authenticated(&self) -> bool { ··· 156 pub fn get_user_did(&self) -> Option<String> { 157 self.session.as_ref().map(|s| s.did.clone()) 158 } 159 + }
+4 -6
src/tui.rs
··· 21 22 use crate::client::AtProtoClient; 23 24 - #[derive(Debug, Clone, PartialEq)] 25 pub struct Message { 26 pub handle: String, 27 pub content: String, ··· 263 "you".to_string(), 264 message, 265 true, 266 - Some( 267 - DateTime::parse_from_rfc3339(&t) 268 - .unwrap() 269 - .with_timezone(&Utc), 270 - ), 271 )); 272 } 273 Err(e) => {
··· 21 22 use crate::client::AtProtoClient; 23 24 + #[derive(Debug, Clone)] 25 pub struct Message { 26 pub handle: String, 27 pub content: String, ··· 263 "you".to_string(), 264 message, 265 true, 266 + DateTime::parse_from_rfc3339(&t) 267 + .map(|dt| dt.with_timezone(&Utc)) 268 + .ok(), // Parse RFC3339 → UTC, None if invalid (so current timestamp instead) 269 )); 270 } 271 Err(e) => {