diff --git a/Cargo.lock b/Cargo.lock index 261eca9e..ab037549 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -148,22 +148,6 @@ dependencies = [ "wasm-bindgen-test", ] -[[package]] -name = "atrium-cli" -version = "0.1.16" -dependencies = [ - "anyhow", - "async-trait", - "atrium-api", - "atrium-xrpc-client", - "chrono", - "clap", - "dirs", - "serde", - "serde_json", - "tokio", -] - [[package]] name = "atrium-xrpc" version = "0.11.1" @@ -238,6 +222,21 @@ version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +[[package]] +name = "bsky-cli" +version = "0.1.16" +dependencies = [ + "anyhow", + "async-trait", + "bsky-sdk", + "chrono", + "clap", + "dirs", + "serde", + "serde_json", + "tokio", +] + [[package]] name = "bsky-sdk" version = "0.1.1" diff --git a/Cargo.toml b/Cargo.toml index fc01dacb..f1c38741 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [workspace] members = [ "atrium-api", - "atrium-cli", "atrium-xrpc", "atrium-xrpc-client", + "bsky-cli", "bsky-sdk", ] # Examples show how to use the latest published crates, not the workspace state. @@ -25,6 +25,7 @@ keywords = ["atproto", "bluesky"] atrium-api = { version = "0.23.0", path = "atrium-api" } atrium-xrpc = { version = "0.11.1", path = "atrium-xrpc" } atrium-xrpc-client = { version = "0.5.5", path = "atrium-xrpc-client" } +bsky-sdk = { version = "0.1.1", path = "bsky-sdk" } # async in traits # Can be removed once MSRV is at least 1.75.0. diff --git a/README.md b/README.md index bc57140f..0822a1f0 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,15 @@ Definitions for XRPC request/response, and their associated errors. A library provides clients that implement the `XrpcClient` defined in [atrium-xrpc](./atrium-xrpc/) -### [`atrium-cli`](./atrium-cli/) +### [`bsky-sdk`](./bsky-sdk/) -[![](https://img.shields.io/crates/v/atrium-cli)](https://crates.io/crates/atrium-cli) +[![](https://img.shields.io/crates/v/bsky-sdk)](https://crates.io/crates/bsky-sdk) + +ATrium-based SDK for Bluesky. + +### [`bsky-cli`](./bsky-cli/) + +[![](https://img.shields.io/crates/v/bsky-cli)](https://crates.io/crates/bsky-cli) A command-line app using this API library. diff --git a/atrium-cli/src/lib.rs b/atrium-cli/src/lib.rs deleted file mode 100644 index 824e4ddd..00000000 --- a/atrium-cli/src/lib.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod commands; -pub mod runner; -pub mod store; diff --git a/atrium-cli/src/store.rs b/atrium-cli/src/store.rs deleted file mode 100644 index e7e52d10..00000000 --- a/atrium-cli/src/store.rs +++ /dev/null @@ -1,42 +0,0 @@ -use async_trait::async_trait; -use atrium_api::agent::{store::SessionStore, Session}; -use std::path::{Path, PathBuf}; -use tokio::fs::{remove_file, File}; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; - -pub struct SimpleJsonFileSessionStore -where - T: AsRef, -{ - path: T, -} - -impl SimpleJsonFileSessionStore -where - T: AsRef, -{ - pub fn new(path: T) -> Self { - Self { path } - } -} - -#[async_trait] -impl SessionStore for SimpleJsonFileSessionStore -where - T: AsRef + Send + Sync + 'static, -{ - async fn get_session(&self) -> Option { - let mut file = File::open(self.path.as_ref()).await.ok()?; - let mut buffer = Vec::new(); - file.read_to_end(&mut buffer).await.ok()?; - serde_json::from_slice(&buffer).ok() - } - async fn set_session(&self, session: Session) { - let mut file = File::create(self.path.as_ref()).await.unwrap(); - let buffer = serde_json::to_vec_pretty(&session).ok().unwrap(); - file.write_all(&buffer).await.ok(); - } - async fn clear_session(&self) { - remove_file(self.path.as_ref()).await.ok(); - } -} diff --git a/atrium-cli/.gitignore b/bsky-cli/.gitignore similarity index 100% rename from atrium-cli/.gitignore rename to bsky-cli/.gitignore diff --git a/atrium-cli/CHANGELOG.md b/bsky-cli/CHANGELOG.md similarity index 100% rename from atrium-cli/CHANGELOG.md rename to bsky-cli/CHANGELOG.md diff --git a/atrium-cli/Cargo.toml b/bsky-cli/Cargo.toml similarity index 71% rename from atrium-cli/Cargo.toml rename to bsky-cli/Cargo.toml index b3370546..6d0c1b24 100644 --- a/atrium-cli/Cargo.toml +++ b/bsky-cli/Cargo.toml @@ -1,20 +1,19 @@ [package] -name = "atrium-cli" +name = "bsky-cli" version = "0.1.16" authors = ["sugyan "] edition.workspace = true rust-version.workspace = true -description = "CLI application for AT Protocol using ATrium API" +description = "CLI application for Bluesky using ATrium API" readme = "README.md" repository.workspace = true license.workspace = true -keywords.workspace = true +keywords = ["atproto", "bluesky", "atrium", "cli"] [dependencies] anyhow.workspace = true async-trait.workspace = true -atrium-api.workspace = true -atrium-xrpc-client.workspace = true +bsky-sdk.workspace = true chrono.workspace = true clap.workspace = true dirs.workspace = true @@ -23,5 +22,5 @@ serde_json.workspace = true tokio = { workspace = true, features = ["full"] } [[bin]] -name = "atrium-cli" +name = "bsky-cli" path = "src/bin/main.rs" diff --git a/atrium-cli/README.md b/bsky-cli/README.md similarity index 88% rename from atrium-cli/README.md rename to bsky-cli/README.md index e630638e..36d544a9 100644 --- a/atrium-cli/README.md +++ b/bsky-cli/README.md @@ -1,11 +1,11 @@ -# ATrium CLI +# Bsky CLI -[![](https://img.shields.io/crates/v/atrium-cli)](https://crates.io/crates/atrium-cli) +[![](https://img.shields.io/crates/v/bsky-cli)](https://crates.io/crates/bsky-cli) -CLI application for AT Protocol using ATrium API +CLI application for Bluesky using ATrium API ``` -Usage: atrium-cli [OPTIONS] +Usage: bsky-cli [OPTIONS] Commands: login Login (Create an authentication session) diff --git a/atrium-cli/config.toml.example b/bsky-cli/config.toml.example similarity index 100% rename from atrium-cli/config.toml.example rename to bsky-cli/config.toml.example diff --git a/atrium-cli/src/bin/main.rs b/bsky-cli/src/bin/main.rs similarity index 71% rename from atrium-cli/src/bin/main.rs rename to bsky-cli/src/bin/main.rs index 7a86929f..1064c831 100644 --- a/atrium-cli/src/bin/main.rs +++ b/bsky-cli/src/bin/main.rs @@ -1,4 +1,4 @@ -use atrium_cli::runner::Runner; +use bsky_cli::{Command, Runner}; use clap::Parser; use std::fmt::Debug; @@ -20,16 +20,19 @@ struct Args { debug: bool, #[command(subcommand)] // command: Command, - command: atrium_cli::commands::Command, + command: Command, } #[tokio::main] async fn main() -> Result<(), Box> { let args = Args::parse(); - Ok( - Runner::new(args.pds_host, args.limit.try_into()?, args.debug) - .await? - .run(args.command) - .await?, + Ok(Runner::new( + args.pds_host, + args.limit.try_into()?, + args.debug, + matches!(args.command, Command::Login(_)), ) + .await? + .run(args.command) + .await?) } diff --git a/atrium-cli/src/commands.rs b/bsky-cli/src/commands.rs similarity index 98% rename from atrium-cli/src/commands.rs rename to bsky-cli/src/commands.rs index 3c74213e..d9e23874 100644 --- a/atrium-cli/src/commands.rs +++ b/bsky-cli/src/commands.rs @@ -1,4 +1,4 @@ -use atrium_api::types::string::AtIdentifier; +use bsky_sdk::api::types::string::AtIdentifier; use clap::Parser; use std::path::PathBuf; use std::str::FromStr; diff --git a/bsky-cli/src/lib.rs b/bsky-cli/src/lib.rs new file mode 100644 index 00000000..c98265d3 --- /dev/null +++ b/bsky-cli/src/lib.rs @@ -0,0 +1,5 @@ +mod commands; +mod runner; + +pub use commands::Command; +pub use runner::Runner; diff --git a/atrium-cli/src/runner.rs b/bsky-cli/src/runner.rs similarity index 69% rename from atrium-cli/src/runner.rs rename to bsky-cli/src/runner.rs index 4213dbe3..7236e6d7 100644 --- a/atrium-cli/src/runner.rs +++ b/bsky-cli/src/runner.rs @@ -1,12 +1,12 @@ use crate::commands::Command; -use crate::store::SimpleJsonFileSessionStore; use anyhow::{Context, Result}; -use atrium_api::agent::bluesky::{AtprotoServiceType, BSKY_CHAT_DID}; -use atrium_api::agent::{store::SessionStore, AtpAgent}; -use atrium_api::records::{KnownRecord, Record}; -use atrium_api::types::string::{AtIdentifier, Datetime, Handle}; -use atrium_api::types::LimitedNonZeroU8; -use atrium_xrpc_client::reqwest::ReqwestClient; +use api::agent::bluesky::{AtprotoServiceType, BSKY_CHAT_DID}; +use api::records::{KnownRecord, Record}; +use api::types::string::{AtIdentifier, Datetime, Handle}; +use api::types::LimitedNonZeroU8; +use bsky_sdk::agent::config::{Config, FileStore}; +use bsky_sdk::api; +use bsky_sdk::BskyAgent; use serde::Serialize; use std::ffi::OsStr; use std::path::PathBuf; @@ -14,33 +14,51 @@ use tokio::fs::{create_dir_all, File}; use tokio::io::AsyncReadExt; pub struct Runner { - agent: AtpAgent, + agent: BskyAgent, limit: LimitedNonZeroU8<100>, debug: bool, - session_path: PathBuf, - handle: Option, + config_path: PathBuf, } impl Runner { - pub async fn new(pds_host: String, limit: LimitedNonZeroU8<100>, debug: bool) -> Result { + pub async fn new( + pds_host: String, + limit: LimitedNonZeroU8<100>, + debug: bool, + is_login: bool, + ) -> Result { let config_dir = dirs::config_dir() .with_context(|| format!("No config dir: {:?}", dirs::config_dir()))?; - let dir = config_dir.join("atrium-cli"); + let dir = config_dir.join("bsky-cli"); create_dir_all(&dir).await?; - let session_path = dir.join("session.json"); - let store = SimpleJsonFileSessionStore::new(session_path.clone()); - let session = store.get_session().await; - let handle = session.as_ref().map(|s| s.handle.clone()); - let agent = AtpAgent::new(ReqwestClient::new(pds_host), store); - if let Some(s) = &session { - agent.resume_session(s.clone()).await?; - } + let config_path = dir.join("config.json"); + + let agent = if is_login { + BskyAgent::builder() + .config(Config { + endpoint: pds_host, + ..Default::default() + }) + .build() + .await? + } else { + let store = FileStore::new(&config_path); + let agent = BskyAgent::builder() + .config( + Config::load(&store) + .await + .with_context(|| "Not logged in")?, + ) + .build() + .await?; + agent.to_config().await.save(&store).await?; + agent + }; Ok(Self { agent, limit, debug, - session_path, - handle, + config_path, }) } pub async fn run(&self, command: Command) -> Result<()> { @@ -48,7 +66,16 @@ impl Runner { match command { Command::Login(args) => { self.agent.login(args.identifier, args.password).await?; - println!("Login successful! Saved session to {:?}", self.session_path); + // Set labelers from preferences + let preferences = self.agent.get_preferences(true).await?; + self.agent.configure_labelers_from_preferences(&preferences); + // Save config to file + self.agent + .to_config() + .await + .save(&FileStore::new(&self.config_path)) + .await?; + println!("Login successful! Saved config to {:?}", self.config_path); Ok(()) } Command::GetTimeline => self.print( @@ -59,7 +86,7 @@ impl Runner { .bsky .feed .get_timeline( - atrium_api::app::bsky::feed::get_timeline::ParametersData { + api::app::bsky::feed::get_timeline::ParametersData { algorithm: None, cursor: None, limit: Some(limit), @@ -76,11 +103,8 @@ impl Runner { .bsky .feed .get_author_feed( - atrium_api::app::bsky::feed::get_author_feed::ParametersData { - actor: args - .actor - .or(self.handle.clone().map(AtIdentifier::Handle)) - .with_context(|| "Not logged in")?, + api::app::bsky::feed::get_author_feed::ParametersData { + actor: args.actor.unwrap_or(self.handle().await?.into()), cursor: None, filter: None, limit: Some(limit), @@ -97,7 +121,7 @@ impl Runner { .bsky .feed .get_likes( - atrium_api::app::bsky::feed::get_likes::ParametersData { + api::app::bsky::feed::get_likes::ParametersData { cid: None, cursor: None, limit: Some(limit), @@ -115,7 +139,7 @@ impl Runner { .bsky .feed .get_reposted_by( - atrium_api::app::bsky::feed::get_reposted_by::ParametersData { + api::app::bsky::feed::get_reposted_by::ParametersData { cid: None, cursor: None, limit: Some(limit), @@ -133,11 +157,8 @@ impl Runner { .bsky .feed .get_actor_feeds( - atrium_api::app::bsky::feed::get_actor_feeds::ParametersData { - actor: args - .actor - .or(self.handle.clone().map(AtIdentifier::Handle)) - .with_context(|| "Not logged in")?, + api::app::bsky::feed::get_actor_feeds::ParametersData { + actor: args.actor.unwrap_or(self.handle().await?.into()), cursor: None, limit: Some(limit), } @@ -153,7 +174,7 @@ impl Runner { .bsky .feed .get_feed( - atrium_api::app::bsky::feed::get_feed::ParametersData { + api::app::bsky::feed::get_feed::ParametersData { cursor: None, feed: args.uri.to_string(), limit: Some(limit), @@ -170,7 +191,7 @@ impl Runner { .bsky .feed .get_list_feed( - atrium_api::app::bsky::feed::get_list_feed::ParametersData { + api::app::bsky::feed::get_list_feed::ParametersData { cursor: None, limit: Some(limit), list: args.uri.to_string(), @@ -187,11 +208,8 @@ impl Runner { .bsky .graph .get_follows( - atrium_api::app::bsky::graph::get_follows::ParametersData { - actor: args - .actor - .or(self.handle.clone().map(AtIdentifier::Handle)) - .with_context(|| "Not logged in")?, + api::app::bsky::graph::get_follows::ParametersData { + actor: args.actor.unwrap_or(self.handle().await?.into()), cursor: None, limit: Some(limit), } @@ -207,11 +225,8 @@ impl Runner { .bsky .graph .get_followers( - atrium_api::app::bsky::graph::get_followers::ParametersData { - actor: args - .actor - .or(self.handle.clone().map(AtIdentifier::Handle)) - .with_context(|| "Not logged in")?, + api::app::bsky::graph::get_followers::ParametersData { + actor: args.actor.unwrap_or(self.handle().await?.into()), cursor: None, limit: Some(limit), } @@ -227,11 +242,8 @@ impl Runner { .bsky .graph .get_lists( - atrium_api::app::bsky::graph::get_lists::ParametersData { - actor: args - .actor - .or(self.handle.clone().map(AtIdentifier::Handle)) - .with_context(|| "Not logged in")?, + api::app::bsky::graph::get_lists::ParametersData { + actor: args.actor.unwrap_or(self.handle().await?.into()), cursor: None, limit: Some(limit), } @@ -247,7 +259,7 @@ impl Runner { .bsky .graph .get_list( - atrium_api::app::bsky::graph::get_list::ParametersData { + api::app::bsky::graph::get_list::ParametersData { cursor: None, limit: Some(limit), list: args.uri.to_string(), @@ -264,11 +276,8 @@ impl Runner { .bsky .actor .get_profile( - atrium_api::app::bsky::actor::get_profile::ParametersData { - actor: args - .actor - .or(self.handle.clone().map(AtIdentifier::Handle)) - .with_context(|| "Not logged in")?, + api::app::bsky::actor::get_profile::ParametersData { + actor: args.actor.unwrap_or(self.handle().await?.into()), } .into(), ) @@ -282,7 +291,7 @@ impl Runner { .bsky .actor .get_preferences( - atrium_api::app::bsky::actor::get_preferences::ParametersData {}.into(), + api::app::bsky::actor::get_preferences::ParametersData {}.into(), ) .await?, ), @@ -294,7 +303,7 @@ impl Runner { .bsky .notification .list_notifications( - atrium_api::app::bsky::notification::list_notifications::ParametersData { + api::app::bsky::notification::list_notifications::ParametersData { cursor: None, limit: Some(limit), seen_at: None, @@ -314,7 +323,7 @@ impl Runner { .bsky .convo .list_convos( - atrium_api::chat::bsky::convo::list_convos::ParametersData { + api::chat::bsky::convo::list_convos::ParametersData { cursor: None, limit: Some(limit), } @@ -324,21 +333,22 @@ impl Runner { ), Command::SendConvoMessage(args) => { let did = match args.actor { - AtIdentifier::Handle(handle) => self - .agent - .api - .com - .atproto - .identity - .resolve_handle( - atrium_api::com::atproto::identity::resolve_handle::ParametersData { - handle: handle.clone(), - } - .into(), - ) - .await? - .data - .did, + AtIdentifier::Handle(handle) => { + self.agent + .api + .com + .atproto + .identity + .resolve_handle( + api::com::atproto::identity::resolve_handle::ParametersData { + handle: handle.clone(), + } + .into(), + ) + .await? + .data + .did + } AtIdentifier::Did(did) => did, }; let chat = &self @@ -352,7 +362,7 @@ impl Runner { .bsky .convo .get_convo_for_members( - atrium_api::chat::bsky::convo::get_convo_for_members::ParametersData { + api::chat::bsky::convo::get_convo_for_members::ParametersData { members: vec![did], } .into(), @@ -363,9 +373,9 @@ impl Runner { .bsky .convo .send_message( - atrium_api::chat::bsky::convo::send_message::InputData { + api::chat::bsky::convo::send_message::InputData { convo_id: convo.data.convo.data.id, - message: atrium_api::chat::bsky::convo::defs::MessageInputData { + message: api::chat::bsky::convo::defs::MessageInputData { embed: None, facets: None, text: args.text, @@ -393,7 +403,7 @@ impl Runner { .await .expect("upload blob"); images.push( - atrium_api::app::bsky::embed::images::ImageData { + api::app::bsky::embed::images::ImageData { alt: image .file_name() .map(OsStr::to_string_lossy) @@ -406,10 +416,10 @@ impl Runner { ) } } - let embed = Some(atrium_api::types::Union::Refs( - atrium_api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedImagesMain( - Box::new(atrium_api::app::bsky::embed::images::MainData { images }.into()), - ), + let embed = Some(api::types::Union::Refs( + api::app::bsky::feed::post::RecordEmbedRefs::AppBskyEmbedImagesMain(Box::new( + api::app::bsky::embed::images::MainData { images }.into(), + )), )); self.print( &self @@ -419,10 +429,10 @@ impl Runner { .atproto .repo .create_record( - atrium_api::com::atproto::repo::create_record::InputData { + api::com::atproto::repo::create_record::InputData { collection: "app.bsky.feed.post".parse().expect("valid"), record: Record::Known(KnownRecord::AppBskyFeedPost(Box::new( - atrium_api::app::bsky::feed::post::RecordData { + api::app::bsky::feed::post::RecordData { created_at: Datetime::now(), embed, entities: None, @@ -435,7 +445,7 @@ impl Runner { } .into(), ))), - repo: self.handle.clone().with_context(|| "Not logged in")?.into(), + repo: self.handle().await?.into(), rkey: None, swap_commit: None, validate: None, @@ -453,9 +463,9 @@ impl Runner { .atproto .repo .delete_record( - atrium_api::com::atproto::repo::delete_record::InputData { + api::com::atproto::repo::delete_record::InputData { collection: "app.bsky.feed.post".parse().expect("valid"), - repo: self.handle.clone().with_context(|| "Not logged in")?.into(), + repo: self.handle().await?.into(), rkey: args.uri.rkey, swap_commit: None, swap_record: None, @@ -474,4 +484,13 @@ impl Runner { } Ok(()) } + async fn handle(&self) -> Result { + Ok(self + .agent + .get_session() + .await + .with_context(|| "Not logged in")? + .data + .handle) + } } diff --git a/bsky-sdk/src/agent/config.rs b/bsky-sdk/src/agent/config.rs index 2b64ad4a..a118ae99 100644 --- a/bsky-sdk/src/agent/config.rs +++ b/bsky-sdk/src/agent/config.rs @@ -8,7 +8,7 @@ pub use file::FileStore; use serde::{Deserialize, Serialize}; /// Configuration data struct for the [`BskyAgent`](super::BskyAgent). -#[derive(Debug, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { /// The base URL for the XRPC endpoint. pub endpoint: String, diff --git a/release-plz.toml b/release-plz.toml index bc89019a..736e1c94 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -26,14 +26,14 @@ git_tag_enable = true changelog_update = true [[package]] -name = "atrium-cli" +name = "bsky-sdk" publish = true git_release_enable = true git_tag_enable = true changelog_update = true [[package]] -name = "bsky-sdk" +name = "bsky-cli" publish = true git_release_enable = true git_tag_enable = true