cli-sky 0.5.0

A CLI AT protocol client
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
use std::fs::File;
use std::io::Write;
use std::process;
use std::fs;
use std::io::{self, BufRead};
use std::str::FromStr;

use atrium_identity::identity_resolver::IdentityResolverConfig;
use cli_sky::com;
use keyring::error::Error;
use bsky_sdk::BskyAgent;
use atrium_api::types::string::Datetime;
use atrium_api::app::bsky::feed::get_timeline::ParametersData;
use bsky_sdk::agent::config::{Config, FileStore};
use atrium_api::app::bsky::feed::post::RecordData as PostRecordData;
use serde_json::from_value;
use serde::{Deserialize, Serialize};
use cli_sky::lexicon::record::KnownRecord;
use cli_sky::lexicon::wrapper::AtpServiceClientWrapper;
use atrium_api::com::atproto::repo::create_record::InputData;
use atrium_api::app::bsky::feed::get_author_feed::Parameters;

use atrium_xrpc_client::reqwest::ReqwestClientBuilder;
use atrium_api::app::bsky::feed::get_author_feed;
use atrium_identity::identity_resolver::IdentityResolver;
use atrium_api::client::AtpServiceClient;
use atrium_xrpc_client::reqwest::ReqwestClient;

async fn print_logo() {
    println!("{}[2J", 27 as char);
    println!("  /$$$$$$  /$$       /$$$$$$        /$$$$$$  /$$   /$$ /$$     /$$");
    println!(" /$$__  $$| $$      |_  $$_/       /$$__  $$| $$  /$$/|  $$   /$$/");
    println!("| $$  |__/| $$        | $$        | $$  |__/| $$ /$$/  |  $$ /$$/ ");
    println!("| $$      | $$        | $$        |  $$$$$$ | $$$$$/    |  $$$$/  ");
    println!("| $$      | $$        | $$         |____  $$| $$  $$     |  $$/   ");
    println!("| $$    $$| $$        | $$         /$$  | $$| $$|  $$     | $$    ");
    println!("|  $$$$$$/| $$$$$$$$ /$$$$$$      |  $$$$$$/| $$ |  $$    | $$    ");
    println!(" |______/ |________/|______/       |______/ |__/  |__/    |__/   ");
    println!("");
    println!("");
}

#[derive(Serialize, Deserialize)]
struct BlogPost {
    title: String,
    text: String,
    tags: Option<Vec<String>>,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {

    print_logo().await;

    ask_to_login().await?;
    Ok(())
}

async fn login() -> Result<(), Box<dyn std::error::Error>> {
    println!("Welcome!!! Please enter your AT Protocol Handle");

    loop {
        println!("Login:");

        let mut uname = String::new();
        std::io::stdin().read_line(&mut uname).expect("Failed to read");

        println!("");
        println!("what is your password?");

   //     std::io::stdout().flush().unwrap();
     //   let pwd = read_password().unwrap();
       // println!("{pwd}");

       let pwd = rpassword::prompt_password("Password: ").unwrap();
       println!("{pwd}");

        let agent = create_agent(uname.trim().to_string(), pwd.to_string()).await?;


        match start_session(agent).await {
            Ok(_) => {
                // Session started successfully
                return Ok(());
            }
            Err(e) => {
                println!("\nLogin failed. Please check your handle and password.");
                println!("{e}");
                // Optionally, you could log the detailed error for debugging:
                // eprintln!("Error details: {}", e);
                continue;
            }
        }
    }
}

async fn ask_to_login() -> Result<(), Box<dyn std::error::Error>> {
    let service = "cli_sky";
    let username = "user";
    let entry = keyring::Entry::new(service, username)?;

    let mut pwd = String::new();


    match entry.get_password() {
        Ok(secret) => {pwd = secret},
        Err(Error::NoEntry) => {
            login().await?;
        }
        Err(e) => {
            eprintln!("Failed to get password: {}", e);
        }
    }

    println!("Got secret: {pwd}");

    let mut file = File::create("config.json")?;
    file.write_all(pwd.as_bytes())?;

    let agent = BskyAgent::builder()
    .config(Config::load(&FileStore::new("config.json")).await?)
    .build()
    .await?;

    match start_session(agent).await {
        Ok(_) => {
            // Session started successfully
            return Ok(());
        }
        Err(e) => {
            println!("\nLogin failed. Please enter new details.");
            println!("{e}");
            login().await?;
            Ok(())
        }
    }
}

async fn create_agent(uname: String, pwd: String) -> Result<BskyAgent, Box<dyn std::error::Error>> {
    let agent = BskyAgent::builder().build().await?;
    let session = agent.login(&uname,&pwd).await?;
    println!("Logged in! DID = {}", session.did.to_string());
    return Ok(agent);
}

async fn start_session(agent: BskyAgent) -> Result<(), Box<dyn std::error::Error>> {
    save_session(&agent).await?;


    match menu(agent).await {
        Ok(_) => {
            // Session started successfully
            return Ok(());
        }
        Err(e) => {
            println!("there is an error here");
            return Err(e);
        }
    }

}

async fn save_session(agent: &BskyAgent) -> Result<(), Box<dyn std::error::Error>> {
    agent.to_config()
    .await
    .save(&FileStore::new("config.json"))
    .await?;
    println!("Session saved to config.json");

    //deserialize the json
    let config = fs::read_to_string("config.json")?;

    //create an entry
    let service = "cli_sky";
    let username = "user";
    let entry = keyring::Entry::new(service, username)?;
    entry.set_password(&config)?;

    //delete the config.json file
    fs::remove_file("config.json")?;

    Ok(())
}

fn menu(agent: BskyAgent) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<(), Box<dyn std::error::Error>>> + Send>> {
    Box::pin(async move {
        print!("{}[2J", 27 as char);
        print_logo().await;
        println!("");
        println!("What would you like to do?");
        println!("0: Exit");
        println!("1: Text Post");
        println!("2: Following Feed");
        println!("3: Blog Post");
        println!("4: Find a Blog");
        println!("");

        let mut input = String::new();

        loop {
            std::io::stdin().read_line(&mut input).expect("Failed to read menu input");

            match input.trim().parse::<i32>() {
                Ok(0) => {
                    println!("Exiting...");
                    process::exit(0);
                }
                Ok(1) => {
                    println!("writing post");
                    make_post(agent.clone()).await?;  // Assuming agent is cloneable
                    break;
                }
                Ok(2) => {
                    println!("following feed");
                    following_feed(agent.clone()).await?;
                    break;
                }
                Ok(3) => {
                    println!("blog post");
                    write_blog(agent.clone()).await?;
                    break;
                }
                Ok(4) => {
                    println!("find a blog");
                    list_user_blog(&agent).await?;
                    break;
                }
                _ => {
                    input.clear();
                    println!("Invalid input");
                    continue;
                }
            };
        }

        Ok(())
    })
}


async fn make_post(agent: BskyAgent) -> Result<(), Box<dyn std::error::Error>> {
    let mut content = String::new();
    io::stdin().read_line(&mut content).expect("Failed to read post content");


    agent
        .create_record(atrium_api::app::bsky::feed::post::RecordData {
            created_at: Datetime::now(),
            embed: None,
            entities: None,
            facets: None,
            labels: None,
            langs: None,
            reply: None,
            tags: None,
            text: content,
        })
        .await?;

    println!("Post sent to the Atmosphere!!!");
    menu(agent).await?;
    Ok(())
}

async fn following_feed(agent: BskyAgent)-> Result<(), Box<dyn std::error::Error>>{

    // Fetch the first page of timeline (default cursor, default limit)
    let output = agent.clone()
        .api.app.bsky.feed.get_timeline(
            ParametersData {
                cursor: None,
                limit: None,
                algorithm: Some("reverse-chronological".to_string()),
            }
            .into(),
        )
        .await?;

    // Iterate over posts
    for feed_post in &output.feed {
        let author = &feed_post.post.author;

        // Try deserializing the record field into a known post structure
        let record_value = serde_json::to_value::<atrium_api::types::Unknown>(feed_post.post.record.clone())?;
        let maybe_post: Result<PostRecordData, _> =
            from_value(record_value);

        match maybe_post {
            Ok(post_record) => {
                println!("{}[2J", 27 as char);
                println!(
                    "{} \n(@{})\n\n {}",
                    author.display_name.clone().unwrap_or_default(),
                    author.handle.to_string(),
                    post_record.text
                );
                println!("\nType 'exit' to quit.\n");


                let mut input = String::new();
                std::io::stdin().read_line( &mut input).expect("Failed to read");
                match input.trim() {
                    "exit" => {process::exit(0)}
                    "menu" => {menu(agent.clone()).await?},
                    _ => {}
                }
            }
            Err(e) => {
                eprintln!(
                    "Failed to parse post record from {} (@{}): {}",
                    author.display_name.clone().unwrap_or_default(),
                    author.handle.to_string(),
                    e
                );
            }
        }
    }
    process::exit(0);
}

async fn write_blog(agent: BskyAgent) -> Result<(), Box<dyn std::error::Error>> {
    println!("Enter blog title:");
    let mut title = String::new();
    io::stdin().read_line(&mut title)?;
    let title = title.trim().to_string();
    
    println!("Paste blog content in markdown (type END in all caps on a new line to finish):");
    let stdin = io::stdin();
    let mut content = String::new();
    
    for line in stdin.lock().lines() {
        let line = line?;
        if line.trim() == "END" { break; }
        content.push_str(&line);
        content.push('\n');
    }
    let content = content.trim().to_string();
    
    println!("Enter tags (comma-separated, or press Enter for none):");
    let mut tags_input = String::new();
    io::stdin().read_line(&mut tags_input)?;
    let tags: Option<Vec<String>> = if tags_input.trim().is_empty() {
        None
    } else {
        Some(tags_input.trim().split(',').map(|s| s.trim().to_string()).collect())
    };

    let blog_record = BlogPost {
        title: title.clone(),
        text: content.clone(),
        tags: tags.clone(),
    };

    let record_json = serde_json::json!({
        "$type": "com.macroblog.blog.post",
        "title": title,
        "text": content,
        "tags": tags,
    });

    let session_info = agent.api.com.atproto.server.get_session().await?;
    let did = session_info.did.clone(); // or however you get the current user's DID

    let record_unknown: atrium_api::types::Unknown = serde_json::from_value(record_json)?;
    let input = InputData {
        collection: "com.macroblog.blog.post".parse()?, // Your lexicon's NSID
        repo: did.into(),
        rkey: None, // Let the server pick a key
        record: record_unknown,
        swap_commit: None, 
        validate: None,
    };
    agent.api.com.atproto.repo.create_record(input.into()).await?;

    println!("Blog post created successfully!");
    menu(agent).await?;
    Ok(())
} 


pub async fn list_user_blog(agent: &BskyAgent) -> Result<(), Box<dyn std::error::Error>> {
    print!("Enter blogger's handle: ");
    io::stdout().flush()?;

    let mut name = String::new();
    io::stdin().read_line(&mut name)?;
    let handle = name.trim();

    // Get the author's DID
    let did = agent.api.com.atproto.identity.resolve_handle(
        atrium_api::com::atproto::identity::resolve_handle::ParametersData {
            handle: atrium_api::types::string::Handle::from_str(handle)?,
        }
        .into(),
    ).await?.did.clone();

    // Get the blog posts using listRecords
    let response = agent
        .api
        .com
        .atproto
        .repo
        .list_records(
            atrium_api::com::atproto::repo::list_records::ParametersData {
                repo: atrium_api::types::string::AtIdentifier::from_str(&did.to_string())?,
                collection: "com.macroblog.blog.post".parse()?,
                limit: Some(50.try_into()?),
                cursor: None,
                reverse: None,
            }
            .into(),
        )
        .await?;

    // Print out blog posts
    for record in &response.records {
        let record_value = serde_json::to_value::<atrium_api::types::Unknown>(record.value.clone())?;
        
        // Debug logging
        println!("DEBUG: Record type: {:?}", record_value.get("$type"));
        println!("DEBUG: Full record: {:?}", record_value);
        
        // Parse the blog post
        match serde_json::from_value::<BlogPost>(record_value.clone()) {
            Ok(blog_post) => {
                println!("\n{}", "=".repeat(50));
                println!(
                    "Title: {}\n\n{}",
                    blog_post.title,
                    blog_post.text
                );
                if let Some(tags) = blog_post.tags {
                    println!("\nTags: {}", tags.join(", "));
                }
                println!("{}", "=".repeat(50));
            }
            Err(e) => {
                println!("DEBUG: Failed to parse blog post: {}", e);
            }
        }
        let mut dummy = String::new();
        io::stdin().read_line(&mut dummy)?;
    }

    let mut dummy = String::new();
    io::stdin().read_line(&mut dummy)?;  
    menu(agent.clone()).await?;  
    Ok(())
}