Noreposts Feed
at main 154 lines 4.5 kB view raw
1use anyhow::{anyhow, Result}; 2use reqwest::Client; 3use serde::{Deserialize, Serialize}; 4use std::io::{self, Write}; 5 6#[derive(Debug, Serialize)] 7struct LoginRequest { 8 identifier: String, 9 password: String, 10} 11 12#[derive(Debug, Deserialize)] 13struct LoginResponse { 14 #[serde(rename = "accessJwt")] 15 access_jwt: String, 16 did: String, 17 handle: String, 18} 19 20#[derive(Debug, Serialize)] 21struct PutRecordRequest { 22 repo: String, 23 collection: String, 24 rkey: String, 25 record: FeedGeneratorRecord, 26} 27 28#[derive(Debug, Serialize)] 29struct FeedGeneratorRecord { 30 #[serde(rename = "$type")] 31 record_type: String, 32 did: String, 33 #[serde(rename = "displayName")] 34 display_name: String, 35 #[serde(skip_serializing_if = "Option::is_none")] 36 description: Option<String>, 37 #[serde(rename = "createdAt")] 38 created_at: String, 39} 40 41pub async fn publish_feed() -> Result<()> { 42 println!("=== Bluesky Feed Generator Publisher ===\n"); 43 44 // Get user input 45 let handle = prompt("Enter your Bluesky handle: ")?; 46 let password = prompt_password("Enter your Bluesky password (App Password): ")?; 47 let record_name = prompt("Enter a short name for the record (shown in URL): ")?; 48 let display_name = prompt("Enter a display name for your feed: ")?; 49 let description = prompt_optional("Enter a brief description (optional): ")?; 50 51 // Get feed generator DID from environment 52 dotenvy::dotenv().ok(); 53 let feedgen_service_did = std::env::var("FEEDGEN_SERVICE_DID") 54 .or_else(|_| { 55 std::env::var("FEEDGEN_HOSTNAME").map(|hostname| format!("did:web:{}", hostname)) 56 }) 57 .map_err(|_| anyhow!("Please set FEEDGEN_SERVICE_DID or FEEDGEN_HOSTNAME in .env file"))?; 58 59 println!("\nPublishing feed..."); 60 61 let client = Client::new(); 62 let pds_url = "https://bsky.social"; 63 64 // Login to get session 65 let login_response: LoginResponse = client 66 .post(format!("{}/xrpc/com.atproto.server.createSession", pds_url)) 67 .json(&LoginRequest { 68 identifier: handle.clone(), 69 password, 70 }) 71 .send() 72 .await? 73 .json() 74 .await?; 75 76 println!("✓ Logged in as {}", login_response.did); 77 78 // Create feed generator record 79 let record = FeedGeneratorRecord { 80 record_type: "app.bsky.feed.generator".to_string(), 81 did: feedgen_service_did, 82 display_name, 83 description: if description.is_empty() { 84 None 85 } else { 86 Some(description) 87 }, 88 created_at: chrono::Utc::now().to_rfc3339(), 89 }; 90 91 // Publish the record 92 let put_request = PutRecordRequest { 93 repo: login_response.did.clone(), 94 collection: "app.bsky.feed.generator".to_string(), 95 rkey: record_name.clone(), 96 record, 97 }; 98 99 let response = client 100 .post(format!("{}/xrpc/com.atproto.repo.putRecord", pds_url)) 101 .header( 102 "Authorization", 103 format!("Bearer {}", login_response.access_jwt), 104 ) 105 .json(&put_request) 106 .send() 107 .await?; 108 109 if !response.status().is_success() { 110 let error_text = response.text().await?; 111 eprintln!("Error response: {}", error_text); 112 return Err(anyhow!("Failed to publish feed: {}", error_text)); 113 } 114 115 response.error_for_status()?; 116 117 println!("\n✅ Feed published successfully!"); 118 println!( 119 "🔗 Feed AT-URI: at://{}/app.bsky.feed.generator/{}", 120 login_response.did, record_name 121 ); 122 println!("\n🌐 You can view your feed at:"); 123 println!( 124 " https://bsky.app/profile/{}/feed/{}", 125 login_response.handle, record_name 126 ); 127 println!("\nYou can now find and share your feed in the Bluesky app!"); 128 129 Ok(()) 130} 131 132fn prompt(message: &str) -> Result<String> { 133 print!("{}", message); 134 io::stdout().flush()?; 135 let mut input = String::new(); 136 io::stdin().read_line(&mut input)?; 137 Ok(input.trim().to_string()) 138} 139 140fn prompt_optional(message: &str) -> Result<String> { 141 print!("{}", message); 142 io::stdout().flush()?; 143 let mut input = String::new(); 144 io::stdin().read_line(&mut input)?; 145 Ok(input.trim().to_string()) 146} 147 148fn prompt_password(message: &str) -> Result<String> { 149 print!("{}", message); 150 io::stdout().flush()?; 151 let mut password = String::new(); 152 io::stdin().read_line(&mut password)?; 153 Ok(password.trim().to_string()) 154}