Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
6.1 kB · 199 lines
Rust
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199mod client;mod credentials;mod interactive;mod jetstream;mod tui;
use anyhow::{Context, Result};use clap::{Parser, Subcommand};use std::io::{self, Write};
use client::AtProtoClient;use credentials::{CredentialStore, Credentials};
#[derive(Parser)]#[command(name = "thought")]#[command(about = "CLI tool for publishing stream.thought.blip records")]#[command(version = "0.1.0")]struct Cli { #[command(subcommand)] command: Option<Commands>, /// Message to publish (if no subcommand is provided) message: Option<String>,}
#[derive(Subcommand)]enum Commands { /// Login to Bluesky with credentials Login, /// Logout and clear stored credentials Logout, /// Enter interactive mode for rapid posting Interactive { /// Use TUI interface with live message feed #[arg(long)] tui: bool, }, /// Enter the thought stream (TUI mode with live feed) Stream,}
fn prompt_for_input(prompt: &str) -> Result<String> { print!("{}", prompt); io::stdout().flush().context("Failed to flush stdout")?; let mut input = String::new(); io::stdin() .read_line(&mut input) .context("Failed to read input")?; Ok(input.trim().to_string())}
async fn handle_login() -> Result<()> { let store = CredentialStore::new()?; if store.exists() { println!("You are already logged in. Use 'thought logout' to clear credentials first."); return Ok(()); }
println!("Login to Bluesky"); println!("================"); let username = prompt_for_input("Username (handle or email): ")?; if username.is_empty() { anyhow::bail!("Username cannot be empty"); }
let password = rpassword::prompt_password("App Password: ") .context("Failed to read password")?; if password.is_empty() { anyhow::bail!("Password cannot be empty"); }
let pds_uri = prompt_for_input("PDS URI (press Enter for https://bsky.social): ")?; let pds_uri = if pds_uri.is_empty() { "https://bsky.social".to_string() } else { pds_uri };
// Test the credentials by attempting to authenticate println!("Testing credentials..."); let credentials = Credentials::new(username, password, pds_uri); let mut client = AtProtoClient::new(&credentials.pds_uri); client.login(&credentials).await .context("Login failed. Please check your credentials.")?;
// Store credentials and session if login was successful store.store(&credentials)?; if let Some(session) = client.get_session() { store.store_session(session)?; } Ok(())}
async fn handle_logout() -> Result<()> { let store = CredentialStore::new()?; store.clear()?; Ok(())}
async fn handle_publish(message: &str) -> Result<()> { let mut client = create_authenticated_client().await?; let uri = client.publish_blip(message).await?; // Update stored session if it was refreshed let store = CredentialStore::new()?; if let Some(session) = client.get_session() { store.store_session(session)?; } println!("Published: {}", uri); Ok(())}
async fn handle_interactive(use_tui: bool) -> Result<()> { let mut client = create_authenticated_client().await?; interactive::run_interactive_mode(&mut client, use_tui).await?; // Update stored session after interactive mode ends in case it was refreshed let store = CredentialStore::new()?; if let Some(session) = client.get_session() { store.store_session(session)?; } Ok(())}
async fn create_authenticated_client() -> Result<AtProtoClient> { let store = CredentialStore::new()?; let credentials = store.load()? .context("Not logged in. Please run 'thought login' first.")?;
let mut client = AtProtoClient::new(&credentials.pds_uri); // Try to load existing session first if let Some(session) = store.load_session()? { client.set_session(session); // Try to refresh the session to validate it's still good // If refresh fails, we'll fall back to full login if let Err(_) = client.refresh_session().await { // Session is invalid, clear it and perform full login store.clear_session()?; client.login(&credentials).await?; // Save the new session if let Some(session) = client.get_session() { store.store_session(session)?; } } else { // Session refresh succeeded, save the updated session if let Some(session) = client.get_session() { store.store_session(session)?; } } } else { // No existing session, perform full login client.login(&credentials).await?; // Save the new session if let Some(session) = client.get_session() { store.store_session(session)?; } } Ok(client)}
#[tokio::main]async fn main() -> Result<()> { let cli = Cli::parse();
match cli.command { Some(Commands::Login) => handle_login().await?, Some(Commands::Logout) => handle_logout().await?, Some(Commands::Interactive { tui }) => handle_interactive(tui).await?, Some(Commands::Stream) => handle_interactive(true).await?, None => { if let Some(message) = cli.message { handle_publish(&message).await?; } else { println!("Usage:"); println!(" thought login # Login to Bluesky"); println!(" thought logout # Logout and clear credentials"); println!(" thought interactive # Enter simple REPL mode"); println!(" thought stream # Enter TUI mode with live feed"); println!(" thought \"message\" # Publish a blip"); } } }
Ok(())}