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?");
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(_) => {
return Ok(());
}
Err(e) => {
println!("\nLogin failed. Please check your handle and password.");
println!("{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(_) => {
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(_) => {
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");
let config = fs::read_to_string("config.json")?;
let service = "cli_sky";
let username = "user";
let entry = keyring::Entry::new(service, username)?;
entry.set_password(&config)?;
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?; 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>>{
let output = agent.clone()
.api.app.bsky.feed.get_timeline(
ParametersData {
cursor: None,
limit: None,
algorithm: Some("reverse-chronological".to_string()),
}
.into(),
)
.await?;
for feed_post in &output.feed {
let author = &feed_post.post.author;
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();
let record_unknown: atrium_api::types::Unknown = serde_json::from_value(record_json)?;
let input = InputData {
collection: "com.macroblog.blog.post".parse()?, repo: did.into(),
rkey: None, 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();
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();
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?;
for record in &response.records {
let record_value = serde_json::to_value::<atrium_api::types::Unknown>(record.value.clone())?;
println!("DEBUG: Record type: {:?}", record_value.get("$type"));
println!("DEBUG: Full record: {:?}", record_value);
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(())
}