From 424f906f4b28cb8fc07b8be46d4b229c11574e05 Mon Sep 17 00:00:00 2001 From: sugyan Date: Fri, 24 Nov 2023 23:45:10 +0900 Subject: [PATCH 1/3] Fix cli --- atrium-cli/Cargo.toml | 4 +- atrium-cli/src/main.rs | 185 +++++++++++++++++++++++++++++------------ 2 files changed, 132 insertions(+), 57 deletions(-) diff --git a/atrium-cli/Cargo.toml b/atrium-cli/Cargo.toml index d358eb99..68b5c94d 100644 --- a/atrium-cli/Cargo.toml +++ b/atrium-cli/Cargo.toml @@ -5,8 +5,8 @@ edition = "2021" authors = ["sugyan "] [dependencies] -atrium-xrpc = { path = "../atrium-xrpc" } -atrium-api = "0.3" +atrium-api = "0.14" +atrium-xrpc-client = "0.2" chrono = "0.4.24" clap = { version = "4.2.4", features = ["derive"] } serde = "1.0.160" diff --git a/atrium-cli/src/main.rs b/atrium-cli/src/main.rs index 5fdf9fc5..7cba5f1a 100644 --- a/atrium-cli/src/main.rs +++ b/atrium-cli/src/main.rs @@ -1,6 +1,8 @@ +use atrium_api::agent::store::MemorySessionStore; +use atrium_api::agent::AtpAgent; use atrium_api::com::atproto::repo::strong_ref::Main as StrongRef; use atrium_api::records::Record; -use atrium_xrpc::XrpcReqwestClient; +use atrium_xrpc_client::reqwest::ReqwestClient; use chrono::Utc; use clap::Parser; use serde::Serialize; @@ -134,19 +136,12 @@ async fn run( command: Command, debug: bool, ) -> Result<(), Box> { - use atrium_api::com::atproto::server::create_session::{CreateSession, Input}; - let mut client = XrpcReqwestClient::new(host); - let session = client - .create_session(Input { - identifier, - password, - }) - .await?; - client.set_auth(session.access_jwt); + let agent = AtpAgent::new(ReqwestClient::new(host), MemorySessionStore::default()); + let session = agent.login(identifier, password).await?; match command { Command::CreateRecord(record) => { - use atrium_api::com::atproto::repo::create_record::{CreateRecord, Input}; - use atrium_api::com::atproto::repo::get_record::{GetRecord, Parameters}; + use atrium_api::com::atproto::repo::create_record::Input; + use atrium_api::com::atproto::repo::get_record::Parameters; let input = match record { CreateRecordCommand::Post(args) => { use atrium_api::app::bsky::feed::post::{ @@ -154,7 +149,11 @@ async fn run( }; let reply = if let Some(uri) = &args.reply { let ru = RecordUri::try_from(uri.as_str())?; - let record = client + let record = agent + .api + .com + .atproto + .repo .get_record(Parameters { cid: None, collection: ru.collection, @@ -178,12 +177,11 @@ async fn run( }; let embed = if let Some(image) = &args.image { use atrium_api::app::bsky::embed::images::{Image, Main as EmbedImages}; - use atrium_api::com::atproto::repo::upload_blob::UploadBlob; let mut images = Vec::with_capacity(image.len()); for path in image { let mut input = Vec::new(); File::open(path)?.read_to_end(&mut input)?; - let output = client.upload_blob(input).await?; + let output = agent.api.com.atproto.repo.upload_blob(input).await?; images.push(Image { alt: path .canonicalize()? @@ -191,6 +189,7 @@ async fn run( .unwrap() .to_string_lossy() .into(), + aspect_ratio: None, image: output.blob, }) } @@ -207,7 +206,10 @@ async fn run( embed, entities: None, facets: None, + labels: None, + langs: None, reply, + tags: None, text: args.text, })), repo: session.did, @@ -219,7 +221,11 @@ async fn run( CreateRecordCommand::Repost(args) => { use atrium_api::app::bsky::feed::repost::Record as RepostRecord; let ru = RecordUri::try_from(args.uri.as_str())?; - let record = client + let record = agent + .api + .com + .atproto + .repo .get_record(Parameters { cid: None, collection: ru.collection, @@ -245,7 +251,11 @@ async fn run( CreateRecordCommand::Like(args) => { use atrium_api::app::bsky::feed::like::Record as LikeRecord; let ru = RecordUri::try_from(args.uri.as_str())?; - let record = client + let record = agent + .api + .com + .atproto + .repo .get_record(Parameters { cid: None, collection: ru.collection, @@ -283,17 +293,33 @@ async fn run( } } }; - print(client.create_record(input).await?, debug)?; + print( + agent.api.com.atproto.repo.create_record(input).await?, + debug, + )?; } Command::CreateAppPassword { name } => { - use atrium_api::com::atproto::server::create_app_password::{CreateAppPassword, Input}; - print(client.create_app_password(Input { name }).await?, debug)? + use atrium_api::com::atproto::server::create_app_password::Input; + print( + agent + .api + .com + .atproto + .server + .create_app_password(Input { name }) + .await?, + debug, + )? } Command::DeleteRecord { uri } => { - use atrium_api::com::atproto::repo::delete_record::{DeleteRecord, Input}; + use atrium_api::com::atproto::repo::delete_record::Input; let ru = RecordUri::try_from(uri.as_str())?; print( - client + agent + .api + .com + .atproto + .repo .delete_record(Input { collection: ru.collection, repo: ru.did, @@ -305,14 +331,15 @@ async fn run( debug, )? } - Command::GetSession => { - use atrium_api::com::atproto::server::get_session::GetSession; - print(client.get_session().await?, debug)? - } + Command::GetSession => print(agent.api.com.atproto.server.get_session().await?, debug)?, Command::GetProfile { actor } => { - use atrium_api::app::bsky::actor::get_profile::{GetProfile, Parameters}; + use atrium_api::app::bsky::actor::get_profile::Parameters; print( - client + agent + .api + .app + .bsky + .actor .get_profile(Parameters { actor: actor.unwrap_or(session.did), }) @@ -321,10 +348,14 @@ async fn run( )? } Command::GetRecord { uri, cid } => { - use atrium_api::com::atproto::repo::get_record::{GetRecord, Parameters}; + use atrium_api::com::atproto::repo::get_record::Parameters; let ru = RecordUri::try_from(uri.as_str())?; print( - client + agent + .api + .com + .atproto + .repo .get_record(Parameters { cid, collection: ru.collection, @@ -336,9 +367,13 @@ async fn run( )? } Command::GetTimeline => { - use atrium_api::app::bsky::feed::get_timeline::{GetTimeline, Parameters}; + use atrium_api::app::bsky::feed::get_timeline::Parameters; print( - client + agent + .api + .app + .bsky + .feed .get_timeline(Parameters { algorithm: None, cursor: None, @@ -349,9 +384,13 @@ async fn run( )? } Command::GetFollows { actor } => { - use atrium_api::app::bsky::graph::get_follows::{GetFollows, Parameters}; + use atrium_api::app::bsky::graph::get_follows::Parameters; print( - client + agent + .api + .app + .bsky + .graph .get_follows(Parameters { actor: actor.unwrap_or(session.did), cursor: None, @@ -362,9 +401,13 @@ async fn run( )? } Command::GetFollowers { actor } => { - use atrium_api::app::bsky::graph::get_followers::{GetFollowers, Parameters}; + use atrium_api::app::bsky::graph::get_followers::Parameters; print( - client + agent + .api + .app + .bsky + .graph .get_followers(Parameters { actor: actor.unwrap_or(session.did), cursor: None, @@ -375,12 +418,17 @@ async fn run( )? } Command::GetAuthorFeed { author } => { - use atrium_api::app::bsky::feed::get_author_feed::{GetAuthorFeed, Parameters}; + use atrium_api::app::bsky::feed::get_author_feed::Parameters; print( - client + agent + .api + .app + .bsky + .feed .get_author_feed(Parameters { actor: author.unwrap_or(session.did), cursor: None, + filter: None, limit: None, }) .await?, @@ -388,18 +436,30 @@ async fn run( )? } Command::GetPostThread { uri } => { - use atrium_api::app::bsky::feed::get_post_thread::{GetPostThread, Parameters}; + use atrium_api::app::bsky::feed::get_post_thread::Parameters; print( - client - .get_post_thread(Parameters { depth: None, uri }) + agent + .api + .app + .bsky + .feed + .get_post_thread(Parameters { + depth: None, + parent_height: None, + uri, + }) .await?, debug, )? } Command::GetLikes { uri } => { - use atrium_api::app::bsky::feed::get_likes::{GetLikes, Parameters}; + use atrium_api::app::bsky::feed::get_likes::Parameters; print( - client + agent + .api + .app + .bsky + .feed .get_likes(Parameters { cid: None, cursor: None, @@ -411,9 +471,13 @@ async fn run( )? } Command::GetBlocks => { - use atrium_api::app::bsky::graph::get_blocks::{GetBlocks, Parameters}; + use atrium_api::app::bsky::graph::get_blocks::Parameters; print( - client + agent + .api + .app + .bsky + .graph .get_blocks(Parameters { cursor: None, limit: None, @@ -423,11 +487,13 @@ async fn run( )? } Command::ListNotifications => { - use atrium_api::app::bsky::notification::list_notifications::{ - ListNotifications, Parameters, - }; + use atrium_api::app::bsky::notification::list_notifications::Parameters; print( - client + agent + .api + .app + .bsky + .notification .list_notifications(Parameters { cursor: None, limit: None, @@ -437,13 +503,22 @@ async fn run( debug, )? } - Command::ListAppPasswords => { - use atrium_api::com::atproto::server::list_app_passwords::ListAppPasswords; - print(client.list_app_passwords().await?, debug)? - } + Command::ListAppPasswords => print( + agent.api.com.atproto.server.list_app_passwords().await?, + debug, + )?, Command::RevokeAppPassword { name } => { - use atrium_api::com::atproto::server::revoke_app_password::{Input, RevokeAppPassword}; - print(client.revoke_app_password(Input { name }).await?, debug)? + use atrium_api::com::atproto::server::revoke_app_password::Input; + print( + agent + .api + .com + .atproto + .server + .revoke_app_password(Input { name }) + .await?, + debug, + )? } } Ok(()) From eb9bf6ae0e60ee37a8c59bc09286cd2877631b14 Mon Sep 17 00:00:00 2001 From: sugyan Date: Thu, 30 Nov 2023 23:12:47 +0900 Subject: [PATCH 2/3] Renew feed commands --- atrium-cli/Cargo.toml | 5 +- atrium-cli/src/bin/main.rs | 26 ++ atrium-cli/src/commands.rs | 70 +++++ atrium-cli/src/lib.rs | 3 + atrium-cli/src/main.rs | 558 ------------------------------------- atrium-cli/src/runner.rs | 144 ++++++++++ atrium-cli/src/store.rs | 42 +++ 7 files changed, 288 insertions(+), 560 deletions(-) create mode 100644 atrium-cli/src/bin/main.rs create mode 100644 atrium-cli/src/commands.rs create mode 100644 atrium-cli/src/lib.rs delete mode 100644 atrium-cli/src/main.rs create mode 100644 atrium-cli/src/runner.rs create mode 100644 atrium-cli/src/store.rs diff --git a/atrium-cli/Cargo.toml b/atrium-cli/Cargo.toml index 68b5c94d..d365cc1a 100644 --- a/atrium-cli/Cargo.toml +++ b/atrium-cli/Cargo.toml @@ -5,11 +5,12 @@ edition = "2021" authors = ["sugyan "] [dependencies] +async-trait = "0.1.74" atrium-api = "0.14" atrium-xrpc-client = "0.2" chrono = "0.4.24" -clap = { version = "4.2.4", features = ["derive"] } +clap = { version = "4.4.8", features = ["derive"] } +dirs = "5.0.1" serde = "1.0.160" serde_json = "1.0" tokio = { version = "1", features = ["full"] } -toml = "0.7.3" diff --git a/atrium-cli/src/bin/main.rs b/atrium-cli/src/bin/main.rs new file mode 100644 index 00000000..036e8d21 --- /dev/null +++ b/atrium-cli/src/bin/main.rs @@ -0,0 +1,26 @@ +use atrium_cli::runner::Runner; +use clap::Parser; +use std::fmt::Debug; + +#[derive(Parser, Debug)] +#[command(author, version, about)] +struct Args { + #[arg(short, long, default_value = "https://bsky.social")] + pds_host: String, + /// Debug print + #[arg(short, long)] + debug: bool, + #[command(subcommand)] + // command: Command, + command: atrium_cli::commands::Command, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let args = Args::parse(); + Runner::new(args.pds_host, args.debug) + .await? + .run(args.command) + .await; + Ok(()) +} diff --git a/atrium-cli/src/commands.rs b/atrium-cli/src/commands.rs new file mode 100644 index 00000000..be133f1a --- /dev/null +++ b/atrium-cli/src/commands.rs @@ -0,0 +1,70 @@ +use clap::Parser; +use std::str::FromStr; + +#[derive(Parser, Debug)] +pub enum Command { + /// Login (Create an authentication session.) + Login(LoginArgs), + /// Get a view of the actor's home timeline. + GetTimeline, + /// Get a view of an actor's feed. + GetAuthorFeed(ActorArgs), + /// Get the list of likes. + GetLikes(UriArgs), + /// Get a list of reposts. + GetRepostedBy(UriArgs), +} + +#[derive(Parser, Debug)] +pub struct LoginArgs { + /// Handle or other identifier supported by the server for the authenticating user. + #[arg(short, long)] + pub(crate) identifier: String, + /// Password + #[arg(short, long)] + pub(crate) password: String, +} + +#[derive(Parser, Debug)] +pub struct ActorArgs { + /// Actor's handle or did + #[arg(short, long)] + pub(crate) actor: Option, +} + +#[derive(Parser, Debug)] +pub struct UriArgs { + /// Record's URI + #[arg(short, long, value_parser)] + pub(crate) uri: AtUri, +} + +#[derive(Debug, Clone)] +pub(crate) struct AtUri { + pub(crate) did: String, + pub(crate) collection: String, + pub(crate) rkey: String, +} + +impl FromStr for AtUri { + type Err = String; + + fn from_str(s: &str) -> Result { + let parts = s + .strip_prefix("at://did:plc:") + .ok_or(r#"record uri must start with "at://did:plc:""#)? + .splitn(3, '/') + .collect::>(); + Ok(Self { + did: format!("did:plc:{}", parts[0]), + collection: parts[1].to_string(), + rkey: parts[2].to_string(), + }) + } +} + +impl std::fmt::Display for AtUri { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "at://{}/{}/{}", self.did, self.collection, self.rkey) + } +} diff --git a/atrium-cli/src/lib.rs b/atrium-cli/src/lib.rs new file mode 100644 index 00000000..824e4ddd --- /dev/null +++ b/atrium-cli/src/lib.rs @@ -0,0 +1,3 @@ +pub mod commands; +pub mod runner; +pub mod store; diff --git a/atrium-cli/src/main.rs b/atrium-cli/src/main.rs deleted file mode 100644 index 7cba5f1a..00000000 --- a/atrium-cli/src/main.rs +++ /dev/null @@ -1,558 +0,0 @@ -use atrium_api::agent::store::MemorySessionStore; -use atrium_api::agent::AtpAgent; -use atrium_api::com::atproto::repo::strong_ref::Main as StrongRef; -use atrium_api::records::Record; -use atrium_xrpc_client::reqwest::ReqwestClient; -use chrono::Utc; -use clap::Parser; -use serde::Serialize; -use serde_json::to_string_pretty; -use std::fmt::Debug; -use std::fs::{read_to_string, File}; -use std::io::Read; -use std::path::PathBuf; -use toml::{Table, Value}; - -#[derive(Parser, Debug)] -#[command(author, version, about)] -struct Args { - #[arg(short, long, default_value = "https://bsky.social")] - pds_host: String, - /// Path to config file - #[arg(short, long, default_value = "config.toml")] - config: PathBuf, - /// Debug print - #[arg(short, long)] - debug: bool, - #[command(subcommand)] - command: Command, -} - -#[derive(Parser, Debug)] -enum Command { - /// Create a new record (post, repost, like, block) - #[command(subcommand)] - CreateRecord(CreateRecordCommand), - /// Create a new app password - CreateAppPassword { name: String }, - /// Delete record - DeleteRecord { uri: String }, - /// Get current session info - GetSession, - /// Get a profile of an actor (default: current session) - GetProfile { actor: Option }, - /// Get record - GetRecord { uri: String, cid: Option }, - /// Get timeline - GetTimeline, - /// Get following of an actor (default: current session) - GetFollows { actor: Option }, - /// Get followers of an actor (default: current session) - GetFollowers { actor: Option }, - /// Get a feed of an author (default: current session) - GetAuthorFeed { author: Option }, - /// Get a post thread - GetPostThread { uri: String }, - /// Get likes of a record - GetLikes { uri: String }, - /// Get a list of blocking actors - GetBlocks, - /// List notifications - ListNotifications, - /// List app passwords - ListAppPasswords, - /// Revoke an app password - RevokeAppPassword { name: String }, -} - -#[derive(Parser, Debug)] -enum CreateRecordCommand { - /// Create a post - Post(CreateRecordPostArgs), - /// Create a repost - Repost(CreateRecordRepostArgs), - /// Like a record - Like(CreateRecordLikeArgs), - /// Block an actor - Block(CreateRecordBlockArgs), -} - -#[derive(Parser, Debug)] -struct CreateRecordPostArgs { - /// Text of the post - text: String, - /// URI of the post to reply to - #[arg(short, long)] - reply: Option, - /// image files - #[arg(short, long)] - image: Option>, -} - -#[derive(Parser, Debug)] -struct CreateRecordRepostArgs { - /// URI of the post to repost - uri: String, -} - -#[derive(Parser, Debug)] -struct CreateRecordLikeArgs { - /// URI of an record to like - uri: String, -} - -#[derive(Parser, Debug)] -struct CreateRecordBlockArgs { - /// DID of an actor to block - did: String, -} - -#[tokio::main] -async fn main() -> Result<(), Box> { - let args = Args::parse(); - - let value = read_to_string(args.config)?.parse::()?; - if let (Some(Value::String(identifier)), Some(Value::String(password))) = - (value.get("identifier"), value.get("password")) - { - run( - args.pds_host, - identifier.to_string(), - password.to_string(), - args.command, - args.debug, - ) - .await?; - } else { - panic!("invalid config"); - } - Ok(()) -} - -async fn run( - host: String, - identifier: String, - password: String, - command: Command, - debug: bool, -) -> Result<(), Box> { - let agent = AtpAgent::new(ReqwestClient::new(host), MemorySessionStore::default()); - let session = agent.login(identifier, password).await?; - match command { - Command::CreateRecord(record) => { - use atrium_api::com::atproto::repo::create_record::Input; - use atrium_api::com::atproto::repo::get_record::Parameters; - let input = match record { - CreateRecordCommand::Post(args) => { - use atrium_api::app::bsky::feed::post::{ - Record as PostRecord, RecordEmbedEnum, ReplyRef, - }; - let reply = if let Some(uri) = &args.reply { - let ru = RecordUri::try_from(uri.as_str())?; - let record = agent - .api - .com - .atproto - .repo - .get_record(Parameters { - cid: None, - collection: ru.collection, - repo: ru.did, - rkey: ru.rkey, - }) - .await?; - let parent = StrongRef { - cid: record.cid.unwrap(), - uri: record.uri, - }; - let mut root = parent.clone(); - if let Record::AppBskyFeedPost(record) = record.value { - if let Some(reply) = record.reply { - root = reply.root; - } - }; - Some(ReplyRef { parent, root }) - } else { - None - }; - let embed = if let Some(image) = &args.image { - use atrium_api::app::bsky::embed::images::{Image, Main as EmbedImages}; - let mut images = Vec::with_capacity(image.len()); - for path in image { - let mut input = Vec::new(); - File::open(path)?.read_to_end(&mut input)?; - let output = agent.api.com.atproto.repo.upload_blob(input).await?; - images.push(Image { - alt: path - .canonicalize()? - .file_name() - .unwrap() - .to_string_lossy() - .into(), - aspect_ratio: None, - image: output.blob, - }) - } - Some(RecordEmbedEnum::AppBskyEmbedImagesMain(Box::new( - EmbedImages { images }, - ))) - } else { - None - }; - Input { - collection: String::from("app.bsky.feed.post"), - record: Record::AppBskyFeedPost(Box::new(PostRecord { - created_at: Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(), - embed, - entities: None, - facets: None, - labels: None, - langs: None, - reply, - tags: None, - text: args.text, - })), - repo: session.did, - rkey: None, - swap_commit: None, - validate: None, - } - } - CreateRecordCommand::Repost(args) => { - use atrium_api::app::bsky::feed::repost::Record as RepostRecord; - let ru = RecordUri::try_from(args.uri.as_str())?; - let record = agent - .api - .com - .atproto - .repo - .get_record(Parameters { - cid: None, - collection: ru.collection, - repo: ru.did, - rkey: ru.rkey, - }) - .await?; - Input { - collection: String::from("app.bsky.feed.repost"), - record: Record::AppBskyFeedRepost(Box::new(RepostRecord { - created_at: Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(), - subject: StrongRef { - cid: record.cid.unwrap(), - uri: args.uri, - }, - })), - repo: session.did, - rkey: None, - swap_commit: None, - validate: None, - } - } - CreateRecordCommand::Like(args) => { - use atrium_api::app::bsky::feed::like::Record as LikeRecord; - let ru = RecordUri::try_from(args.uri.as_str())?; - let record = agent - .api - .com - .atproto - .repo - .get_record(Parameters { - cid: None, - collection: ru.collection, - repo: ru.did, - rkey: ru.rkey, - }) - .await?; - Input { - collection: String::from("app.bsky.feed.like"), - record: Record::AppBskyFeedLike(Box::new(LikeRecord { - created_at: Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(), - subject: StrongRef { - cid: record.cid.unwrap(), - uri: record.uri, - }, - })), - repo: session.did, - rkey: None, - swap_commit: None, - validate: None, - } - } - CreateRecordCommand::Block(args) => { - use atrium_api::app::bsky::graph::block::Record as BlockRecord; - Input { - collection: String::from("app.bsky.graph.block"), - record: Record::AppBskyGraphBlock(Box::new(BlockRecord { - created_at: Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string(), - subject: args.did, - })), - repo: session.did, - rkey: None, - swap_commit: None, - validate: None, - } - } - }; - print( - agent.api.com.atproto.repo.create_record(input).await?, - debug, - )?; - } - Command::CreateAppPassword { name } => { - use atrium_api::com::atproto::server::create_app_password::Input; - print( - agent - .api - .com - .atproto - .server - .create_app_password(Input { name }) - .await?, - debug, - )? - } - Command::DeleteRecord { uri } => { - use atrium_api::com::atproto::repo::delete_record::Input; - let ru = RecordUri::try_from(uri.as_str())?; - print( - agent - .api - .com - .atproto - .repo - .delete_record(Input { - collection: ru.collection, - repo: ru.did, - rkey: ru.rkey, - swap_commit: None, - swap_record: None, - }) - .await?, - debug, - )? - } - Command::GetSession => print(agent.api.com.atproto.server.get_session().await?, debug)?, - Command::GetProfile { actor } => { - use atrium_api::app::bsky::actor::get_profile::Parameters; - print( - agent - .api - .app - .bsky - .actor - .get_profile(Parameters { - actor: actor.unwrap_or(session.did), - }) - .await?, - debug, - )? - } - Command::GetRecord { uri, cid } => { - use atrium_api::com::atproto::repo::get_record::Parameters; - let ru = RecordUri::try_from(uri.as_str())?; - print( - agent - .api - .com - .atproto - .repo - .get_record(Parameters { - cid, - collection: ru.collection, - repo: ru.did, - rkey: ru.rkey, - }) - .await?, - debug, - )? - } - Command::GetTimeline => { - use atrium_api::app::bsky::feed::get_timeline::Parameters; - print( - agent - .api - .app - .bsky - .feed - .get_timeline(Parameters { - algorithm: None, - cursor: None, - limit: None, - }) - .await?, - debug, - )? - } - Command::GetFollows { actor } => { - use atrium_api::app::bsky::graph::get_follows::Parameters; - print( - agent - .api - .app - .bsky - .graph - .get_follows(Parameters { - actor: actor.unwrap_or(session.did), - cursor: None, - limit: None, - }) - .await?, - debug, - )? - } - Command::GetFollowers { actor } => { - use atrium_api::app::bsky::graph::get_followers::Parameters; - print( - agent - .api - .app - .bsky - .graph - .get_followers(Parameters { - actor: actor.unwrap_or(session.did), - cursor: None, - limit: None, - }) - .await?, - debug, - )? - } - Command::GetAuthorFeed { author } => { - use atrium_api::app::bsky::feed::get_author_feed::Parameters; - print( - agent - .api - .app - .bsky - .feed - .get_author_feed(Parameters { - actor: author.unwrap_or(session.did), - cursor: None, - filter: None, - limit: None, - }) - .await?, - debug, - )? - } - Command::GetPostThread { uri } => { - use atrium_api::app::bsky::feed::get_post_thread::Parameters; - print( - agent - .api - .app - .bsky - .feed - .get_post_thread(Parameters { - depth: None, - parent_height: None, - uri, - }) - .await?, - debug, - )? - } - Command::GetLikes { uri } => { - use atrium_api::app::bsky::feed::get_likes::Parameters; - print( - agent - .api - .app - .bsky - .feed - .get_likes(Parameters { - cid: None, - cursor: None, - limit: None, - uri, - }) - .await?, - debug, - )? - } - Command::GetBlocks => { - use atrium_api::app::bsky::graph::get_blocks::Parameters; - print( - agent - .api - .app - .bsky - .graph - .get_blocks(Parameters { - cursor: None, - limit: None, - }) - .await?, - debug, - )? - } - Command::ListNotifications => { - use atrium_api::app::bsky::notification::list_notifications::Parameters; - print( - agent - .api - .app - .bsky - .notification - .list_notifications(Parameters { - cursor: None, - limit: None, - seen_at: None, - }) - .await?, - debug, - )? - } - Command::ListAppPasswords => print( - agent.api.com.atproto.server.list_app_passwords().await?, - debug, - )?, - Command::RevokeAppPassword { name } => { - use atrium_api::com::atproto::server::revoke_app_password::Input; - print( - agent - .api - .com - .atproto - .server - .revoke_app_password(Input { name }) - .await?, - debug, - )? - } - } - Ok(()) -} - -fn print(value: impl Debug + Serialize, debug: bool) -> Result<(), serde_json::Error> { - if debug { - println!("{:#?}", value); - } else { - println!("{}", to_string_pretty(&value)?); - } - Ok(()) -} - -#[derive(Debug)] -struct RecordUri { - did: String, - collection: String, - rkey: String, -} - -impl TryFrom<&str> for RecordUri { - type Error = String; - - fn try_from(value: &str) -> Result { - let parts = value - .strip_prefix("at://did:plc:") - .ok_or(r#"record uri must start with "at://did:plc:""#)? - .splitn(3, '/') - .collect::>(); - Ok(Self { - did: format!("did:plc:{}", parts[0]), - collection: parts[1].to_string(), - rkey: parts[2].to_string(), - }) - } -} diff --git a/atrium-cli/src/runner.rs b/atrium-cli/src/runner.rs new file mode 100644 index 00000000..cbca100c --- /dev/null +++ b/atrium-cli/src/runner.rs @@ -0,0 +1,144 @@ +use crate::commands::Command; +use crate::store::SimpleJsonFileSessionStore; +use atrium_api::agent::{store::SessionStore, AtpAgent}; +use atrium_api::xrpc::error::{Error, XrpcErrorKind}; +use atrium_xrpc_client::reqwest::ReqwestClient; +use serde::Serialize; +use std::path::PathBuf; +use tokio::fs; + +pub struct Runner { + agent: AtpAgent, + debug: bool, + session_path: PathBuf, + handle: Option, +} + +impl Runner { + pub async fn new(pds_host: String, debug: bool) -> Result> { + let config_dir = dirs::config_dir().unwrap(); + let dir = config_dir.join("atrium-cli"); + fs::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?; + } + Ok(Self { + agent, + debug, + session_path, + handle, + }) + } + pub async fn run(&self, command: Command) { + match command { + Command::Login(args) => { + let result = self.agent.login(args.identifier, args.password).await; + match result { + Ok(_) => { + println!("Login successful! Saved session to {:?}", self.session_path); + } + Err(err) => { + eprintln!("{err:#?}"); + } + } + } + Command::GetTimeline => { + self.print( + &self + .agent + .api + .app + .bsky + .feed + .get_timeline(atrium_api::app::bsky::feed::get_timeline::Parameters { + algorithm: None, + cursor: None, + limit: Some(10), + }) + .await, + ); + } + Command::GetAuthorFeed(args) => { + self.print( + &self + .agent + .api + .app + .bsky + .feed + .get_author_feed(atrium_api::app::bsky::feed::get_author_feed::Parameters { + actor: args.actor.or(self.handle.clone()).unwrap(), + cursor: None, + filter: None, + limit: Some(10), + }) + .await, + ); + } + Command::GetLikes(args) => { + self.print( + &self + .agent + .api + .app + .bsky + .feed + .get_likes(atrium_api::app::bsky::feed::get_likes::Parameters { + cid: None, + cursor: None, + limit: Some(10), + uri: args.uri.to_string(), + }) + .await, + ); + } + Command::GetRepostedBy(args) => { + self.print( + &self + .agent + .api + .app + .bsky + .feed + .get_reposted_by(atrium_api::app::bsky::feed::get_reposted_by::Parameters { + cid: None, + cursor: None, + limit: Some(10), + uri: args.uri.to_string(), + }) + .await, + ); + } + } + } + fn print( + &self, + result: &Result>, + ) { + match result { + Ok(result) => { + if self.debug { + println!("{:#?}", result); + } else { + println!("{}", serde_json::to_string_pretty(result).unwrap()); + } + } + Err(err) => { + if let Error::XrpcResponse(e) = err { + if let Some(XrpcErrorKind::Undefined(body)) = &e.error { + if e.status == 401 && body.error == Some("AuthMissing".into()) { + eprintln!("Login required. Use `atrium-cli login` to login."); + return; + } + } + } + eprintln!("{err:#?}"); + } + } + } +} diff --git a/atrium-cli/src/store.rs b/atrium-cli/src/store.rs new file mode 100644 index 00000000..e7e52d10 --- /dev/null +++ b/atrium-cli/src/store.rs @@ -0,0 +1,42 @@ +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(); + } +} From b050e88316c4466d399673bbc4dc5fdd0e5321e1 Mon Sep 17 00:00:00 2001 From: sugyan Date: Wed, 6 Dec 2023 21:11:52 +0900 Subject: [PATCH 3/3] Update cli --- atrium-cli/README.md | 98 ++++--------------------------- atrium-cli/src/commands.rs | 21 ++++++- atrium-cli/src/runner.rs | 114 +++++++++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 87 deletions(-) diff --git a/atrium-cli/README.md b/atrium-cli/README.md index 14a6cb2a..40af6db0 100644 --- a/atrium-cli/README.md +++ b/atrium-cli/README.md @@ -4,96 +4,22 @@ Usage: atrium-cli [OPTIONS] Commands: - create-record Create a new record (post, repost, like, block) - create-app-password Create a new app password - delete-record Delete record - get-session Get current session info - get-profile Get a profile of an actor (default: current session) - get-record Get record - get-timeline Get timeline - get-follows Get following of an actor (default: current session) - get-followers Get followers of an actor (default: current session) - get-author-feed Get a feed of an author (default: current session) - get-post-thread Get a post thread - get-likes Get likes of a record - get-blocks Get a list of blocking actors - list-notifications List notifications - list-app-passwords List app passwords - revoke-app-password Revoke an app password - help Print this message or the help of the given subcommand(s) + login Login (Create an authentication session) + get-timeline Get a view of the actor's home timeline + get-author-feed Get a view of an actor's feed + get-likes Get the list of likes + get-reposted-by Get a list of reposts + get-follows Get a list of who the actor follows + get-followers Get a list of an actor's followers + get-profile Get detailed profile view of an actor + list-notifications Get a list of notifications + create-post Create a new post + delete-post Delete a post + help Print this message or the help of the given subcommand(s) Options: -p, --pds-host [default: https://bsky.social] - -c, --config Path to config file [default: config.toml] -d, --debug Debug print -h, --help Print help -V, --version Print version ``` - -## sub commands - -``` -Create a new record (post, repost, like, block) - -Usage: atrium-cli create-record - -Commands: - post Create a post - repost Create a repost - like Like a record - block Block an actor - help Print this message or the help of the given subcommand(s) - -Options: - -h, --help Print help -``` - -``` -Create a post - -Usage: atrium-cli create-record post [OPTIONS] - -Arguments: - Text of the post - -Options: - -r, --reply URI of the post to reply to - -i, --image image files - -h, --help Print help -``` - -``` -Create a repost - -Usage: atrium-cli create-record repost - -Arguments: - URI of the post to repost - -Options: - -h, --help Print help -``` - -``` -Like a record - -Usage: atrium-cli create-record like - -Arguments: - URI of an record to like - -Options: - -h, --help Print help -``` - -``` -Block an actor - -Usage: atrium-cli create-record block - -Arguments: - DID of an actor to block - -Options: - -h, --help Print help -``` diff --git a/atrium-cli/src/commands.rs b/atrium-cli/src/commands.rs index be133f1a..0f0772fe 100644 --- a/atrium-cli/src/commands.rs +++ b/atrium-cli/src/commands.rs @@ -3,7 +3,7 @@ use std::str::FromStr; #[derive(Parser, Debug)] pub enum Command { - /// Login (Create an authentication session.) + /// Login (Create an authentication session). Login(LoginArgs), /// Get a view of the actor's home timeline. GetTimeline, @@ -13,6 +13,18 @@ pub enum Command { GetLikes(UriArgs), /// Get a list of reposts. GetRepostedBy(UriArgs), + /// Get a list of who the actor follows. + GetFollows(ActorArgs), + /// Get a list of an actor's followers. + GetFollowers(ActorArgs), + /// Get detailed profile view of an actor. + GetProfile(ActorArgs), + /// Get a list of notifications. + ListNotifications, + /// Create a new post. + CreatePost(CreatePostArgs), + /// Delete a post. + DeletePost(UriArgs), } #[derive(Parser, Debug)] @@ -39,6 +51,13 @@ pub struct UriArgs { pub(crate) uri: AtUri, } +#[derive(Parser, Debug)] +pub struct CreatePostArgs { + /// Post text + #[arg(short, long, value_parser)] + pub(crate) text: String, +} + #[derive(Debug, Clone)] pub(crate) struct AtUri { pub(crate) did: String, diff --git a/atrium-cli/src/runner.rs b/atrium-cli/src/runner.rs index cbca100c..b7b71857 100644 --- a/atrium-cli/src/runner.rs +++ b/atrium-cli/src/runner.rs @@ -3,6 +3,7 @@ use crate::store::SimpleJsonFileSessionStore; use atrium_api::agent::{store::SessionStore, AtpAgent}; use atrium_api::xrpc::error::{Error, XrpcErrorKind}; use atrium_xrpc_client::reqwest::ReqwestClient; +use chrono::Local; use serde::Serialize; use std::path::PathBuf; use tokio::fs; @@ -114,6 +115,119 @@ impl Runner { .await, ); } + Command::GetFollows(args) => { + self.print( + &self + .agent + .api + .app + .bsky + .graph + .get_follows(atrium_api::app::bsky::graph::get_follows::Parameters { + actor: args.actor.or(self.handle.clone()).unwrap(), + cursor: None, + limit: Some(10), + }) + .await, + ); + } + Command::GetFollowers(args) => { + self.print( + &self + .agent + .api + .app + .bsky + .graph + .get_followers(atrium_api::app::bsky::graph::get_followers::Parameters { + actor: args.actor.or(self.handle.clone()).unwrap(), + cursor: None, + limit: Some(10), + }) + .await, + ); + } + Command::GetProfile(args) => { + self.print( + &self + .agent + .api + .app + .bsky + .actor + .get_profile(atrium_api::app::bsky::actor::get_profile::Parameters { + actor: args.actor.or(self.handle.clone()).unwrap(), + }) + .await, + ); + } + Command::ListNotifications => { + self.print( + &self + .agent + .api + .app + .bsky + .notification + .list_notifications( + atrium_api::app::bsky::notification::list_notifications::Parameters { + cursor: None, + limit: Some(10), + seen_at: None, + }, + ) + .await, + ); + } + Command::CreatePost(args) => { + self.print( + &self + .agent + .api + .com + .atproto + .repo + .create_record(atrium_api::com::atproto::repo::create_record::Input { + collection: "app.bsky.feed.post".into(), + record: atrium_api::records::Record::AppBskyFeedPost(Box::new( + atrium_api::app::bsky::feed::post::Record { + created_at: Local::now().to_rfc3339(), + embed: None, + entities: None, + facets: None, + labels: None, + langs: None, + reply: None, + tags: None, + text: args.text, + }, + )), + repo: self.handle.clone().unwrap(), + rkey: None, + swap_commit: None, + validate: None, + }) + .await, + ); + } + Command::DeletePost(args) => { + self.print( + &self + .agent + .api + .com + .atproto + .repo + .delete_record(atrium_api::com::atproto::repo::delete_record::Input { + collection: "app.bsky.feed.post".into(), + repo: self.handle.clone().unwrap(), + rkey: args.uri.rkey, + swap_commit: None, + swap_record: None, + }) + .await, + ); + } } } fn print(