Noreposts Feed
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}