diff --git a/Cargo.toml b/Cargo.toml index 7fc8728..a7f11a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,13 +6,12 @@ edition = "2021" [dependencies] async-std = "1.10.0" +async-stream = "0.3.2" base64 = "0.13.0" futures = "0.3.18" hex = "0.4.3" kuska-handshake = { version = "0.2.0", features = ["async_std"] } kuska-sodiumoxide = "0.2.5-0" -# waiting for a pr merge upstream -kuska-ssb = { path = "../ssb" } +kuska-ssb = { git = "https://github.com/Kuska-ssb/ssb" } serde = { version = "1", features = ["derive"] } serde_json = "1" -async-stream = "0.3.2" \ No newline at end of file diff --git a/README.md b/README.md index fe6088f..521277c 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,66 @@ # golgi -_The Golgi complex (aka. Golgi apparatus or Golgi body) packages proteins into membrane-bound vesicles inside the cell before the vesicles are sent to their destination._ +_The Golgi complex (aka. Golgi apparatus or Golgi body) packages proteins +into membrane-bound vesicles inside the cell before the vesicles are sent +to their destination._ ----- -Golgi is an experimental Scuttlebutt client which uses the [kuska-ssb](https://github.com/Kuska-ssb) libraries and aims to provide a high-level API for interacting with an sbot instance. Development efforts are currently oriented towards [go-sbot](https://github.com/cryptoscope/ssb) interoperability. +## Introduction + +Golgi is an asynchronous, experimental Scuttlebutt client that aims to +facilitate Scuttlebutt application development. It provides a high-level +API for interacting with an sbot instance and uses the +[kuska-ssb](https://github.com/Kuska-ssb) libraries to make RPC calls. +Development efforts are currently oriented towards +[go-sbot](https://github.com/cryptoscope/ssb) interoperability. + +## Features + +Golgi offers the ability to invoke individual RPC methods while also +providing a number of convenience methods which may involve multiple RPC +calls and / or the processing of data received from those calls. The +[`Sbot`](crate::sbot::Sbot) `struct` is the primary means of interacting +with the library. + +Features include the ability to publish messages of various kinds; to +retrieve messages (e.g. `about` and `description` messages) and formulate +queries; to follow, unfollow, block and unblock a peer; to query the social +graph; and to generate pub invite codes. ## Example Usage -```rust -pub async fn run() -> Result<(), GolgiError> { - let mut sbot_client = Sbot::init(None, None).await?; +Basic usage is demonstrated below. Visit the [examples directory](https://git.coopcloud.tech/golgi-ssb/golgi/src/branch/main/examples) in the `golgi` repository for +more comprehensive examples. +```rust +use golgi::GolgiError; +use golgi::sbot::Sbot; + +pub async fn run() -> Result<(), GolgiError> { + // Attempt to connect to an sbot instance using the default IP address, + // port and network key (aka. capabilities key). + let mut sbot_client = Sbot::connect(None, None).await?; + + // Call the `whoami` RPC method to retrieve the public key for the sbot + // identity. let id = sbot_client.whoami().await?; + + // Print the public key (identity) to `stdout`. println!("{}", id); + + // Compose an SSB post message type. + let post = SsbMessageContent::Post { + text: "Biology, eh?!".to_string(), + mentions: None, + }; + + // Publish the post. + let post_msg_reference = sbot_client.publish(post).await?; + + // Print the reference (sigil-link) for the published post. + println!("{}", post_msg_reference); + + Ok(()) } ``` diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000..0ab02c0 --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,98 @@ +use std::process; + +use golgi::{messages::SsbMessageContent, GolgiError, Sbot}; + +// Golgi is an asynchronous library so we must call it from within an +// async function. The `GolgiError` type encapsulates all possible +// error variants for the library. +async fn run() -> Result<(), GolgiError> { + // Attempt to connect to an sbot instance using the default IP address, + // port and network key (aka. capabilities key). + let mut sbot_client = Sbot::init(None, None).await?; + + // Alternatively, we could specify a non-standard IP and port. + // let ip_port = "127.0.0.1:8021".to_string(); + // let mut sbot_client = Sbot::init(Some(ip_port), None).await?; + + // Call the `whoami` RPC method to retrieve the public key for the sbot + // identity. This is our 'local' public key. + let id = sbot_client.whoami().await?; + + // Print the public key (identity) to `stdout`. + println!("whoami: {}", id); + + // Compose an SSB `about` message type. + // The `SsbMessageContent` type has many variants and allows for a high + // degree of control when creating messages. + let name = SsbMessageContent::About { + about: id.clone(), + name: Some("golgi".to_string()), + title: None, + branch: None, + image: None, + description: None, + location: None, + start_datetime: None, + }; + + // Publish the name message. The `publish` method returns a reference to + // the published message. + let name_msg_ref = sbot_client.publish(name).await?; + + // Print the message reference to `stdout`. + println!("name_msg_ref: {}", name_msg_ref); + + // Compose an SSB `post` message type. + let post = SsbMessageContent::Post { + text: "golgi go womp womp".to_string(), + mentions: None, + }; + + // Publish the post. + let post_msg_ref = sbot_client.publish(post).await?; + + // Print the post reference to `stdout`. + println!("post_msg_ref: {}", post_msg_ref); + + // Golgi also exposes convenience methods for some of the most common + // message types. Here we see an example of a convenience method for + // posting a description message. The description is for the local + // identity, ie. we are publishing this about "ourself". + let post_msg_ref = sbot_client + .publish_description("this is a description") + .await?; + + // Print the description message reference to `stdout`. + println!("description: {}", post_msg_ref); + + let author: String = id.clone(); + println!("author: {:?}", author); + + // Retrieve the description for the given public key (identity). + let description = sbot_client.get_description(&author).await?; + + // Print the description to `stdout`. + println!("found description: {:?}", description); + + // Compose and publish another `post` message type. + let post = SsbMessageContent::Post { + text: "golgi go womp womp2".to_string(), + mentions: None, + }; + + let post_msg_ref = sbot_client.publish(post).await?; + println!("post_msg_ref2: {}", post_msg_ref); + + Ok(()) +} + +// Enable an async main function and execute the `run()` function, +// catching any errors and printing them to `stderr` before exiting the +// process. +#[async_std::main] +async fn main() { + if let Err(e) = run().await { + eprintln!("Application error: {}", e); + process::exit(1); + } +} diff --git a/examples/friends.rs b/examples/friends.rs new file mode 100644 index 0000000..759d792 --- /dev/null +++ b/examples/friends.rs @@ -0,0 +1,122 @@ +use std::process; + +use golgi::{ + api::friends::{FriendsHops, RelationshipQuery}, + GolgiError, Sbot, +}; + +// Golgi is an asynchronous library so we must call it from within an +// async function. The `GolgiError` type encapsulates all possible +// error variants for the library. +async fn run() -> Result<(), GolgiError> { + // Attempt to connect to an sbot instance using the default IP address, + // port and network key (aka. capabilities key). + let mut sbot_client = Sbot::init(None, None).await?; + + // Call the `whoami` RPC method to retrieve the public key for the sbot + // identity. This is our 'local' public key. + let id = sbot_client.whoami().await?; + + // Print the public key (identity) to `stdout`. + println!("whoami: {}", id); + + // Define IDs (public keys) to follow and block. + let to_follow = String::from("@5Pt3dKy2HTJ0mWuS78oIiklIX0gBz6BTfEnXsbvke9c=.ed25519"); + let to_block = String::from("@7Y4nwfQmVtAilEzi5knXdS2gilW7cGKSHXdXoT086LM=.ed25519"); + + // Set the relationship of the local identity to the `to_follow` identity. + // In this case, the `set_relationship` method publishes a `contact` + // message which defines following as `true` and blocking as `false`. + // A message reference is returned for the published `contact` message. + let response = sbot_client + .set_relationship(&to_follow, true, false) + .await?; + + // Print the message reference to `stdout`. + println!("follow_response: {:?}", response); + + // Set the relationship of the local identity to the `to_block` identity. + // In this case, the `set_relationship` method publishes a `contact` + // message which defines following as `false` and blocking as `true`. + // A message reference is returned for the published `contact` message. + let response = sbot_client.set_relationship(&to_block, false, true).await?; + + // Print the message reference to `stdout`. + println!("follow_response: {:?}", response); + + // Golgi also exposes convenience methods for following and blocking. + // Here is an example of a simpler way to follow an identity. + let _follow_response = sbot_client.follow(&to_follow).await?; + + // Blocking can be achieved in a similar fashion. + let _block_response = sbot_client.block(&to_block).await?; + + // Get a list of peers within 1 hop of the local identity. + let follows = sbot_client + .friends_hops(FriendsHops { + max: 1, + start: None, + // The `reverse` parameter is not currently implemented in `go-sbot`. + reverse: Some(false), + }) + .await?; + + // Print the list of peers to `stdout`. + println!("follows: {:?}", follows); + + // Determine if an identity (`source`) is following a second identity (`dest`). + // This method will return `true` or `false`. + let mref = sbot_client + .friends_is_following(RelationshipQuery { + source: id.clone(), + dest: to_follow.clone(), + }) + .await?; + + // Print the follow status to `stdout`. + println!("isfollowingmref: {}", mref); + + // Determine if an identity (`source`) is blocking a second identity (`dest`). + let mref = sbot_client + .friends_is_blocking(RelationshipQuery { + source: id.clone(), + dest: to_block.clone(), + }) + .await?; + + // Print the block status to `stdout`. + println!("isblockingmref: {}", mref); + + let mref = sbot_client + .friends_is_blocking(RelationshipQuery { + source: id.clone(), + dest: to_follow, + }) + .await?; + + // Output should be `false`. + println!("isblockingmref(should be false): {}", mref); + + let mref = sbot_client + .friends_is_following(RelationshipQuery { + source: id, + dest: to_block.clone(), + }) + .await?; + + // Output should be `false`. + println!("isfollowingmref(should be false): {}", mref); + + Ok(()) +} + +// Enable an async main function and execute the `run()` function, +// catching any errors and printing them to `stderr` before exiting the +// process. +#[async_std::main] +async fn main() { + if let Err(e) = run().await { + eprintln!("Application error: {}", e); + process::exit(1); + } +} diff --git a/examples/invite.rs b/examples/invite.rs new file mode 100644 index 0000000..5d5881f --- /dev/null +++ b/examples/invite.rs @@ -0,0 +1,76 @@ +use std::process; + +use kuska_ssb::api::dto::content::PubAddress; + +use golgi::{messages::SsbMessageContent, GolgiError, Sbot}; + +// Golgi is an asynchronous library so we must call it from within an +// async function. The `GolgiError` type encapsulates all possible +// error variants for the library. +async fn run() -> Result<(), GolgiError> { + // Attempt to connect to an sbot instance using the default IP address, + // port and network key (aka. capabilities key). + let mut sbot_client = Sbot::init(None, None).await?; + + // Call the `whoami` RPC method to retrieve the public key for the sbot + // identity. This is our 'local' public key. + let id = sbot_client.whoami().await?; + + // Print the public key (identity) to `stdout`. + println!("whoami: {}", id); + + // Compose a `pub` address type message. + let pub_address_msg = SsbMessageContent::Pub { + address: Some(PubAddress { + // IP address. + host: Some("127.0.0.1".to_string()), + // Port. + port: 8009, + // Public key. + key: id, + }), + }; + + // Publish the `pub` address message. + // This step is required for successful invite code creation. + let pub_msg_ref = sbot_client.publish(pub_address_msg).await?; + + // Print the message reference to `stdout`. + println!("pub_msg_ref: {}", pub_msg_ref); + + // Generate an invite code that can be used 1 time. + let invite_code = sbot_client.invite_create(1).await?; + + // Print the invite code to `stdout`. + println!("invite (1 use): {:?}", invite_code); + + // Generate an invite code that can be used 7 times. + let invite_code_2 = sbot_client.invite_create(7).await?; + + // Print the invite code to `stdout`. + println!("invite (7 uses): {:?}", invite_code_2); + + // Define an invite code. + let test_invite = + "net:ssbroom2.commoninternet.net:8009~shs:wm8a1zHWjtESv4XSKMWU/rPRhnAoAiSAe4hQSY0UF5A="; + + // Redeem an invite code (initiating a mutual follow between the local + // identity and the identity which generated the code (`wm8a1z...`). + let mref = sbot_client.invite_use(test_invite).await?; + + // Print the message reference to `stdout`. + println!("mref: {:?}", mref); + + Ok(()) +} + +// Enable an async main function and execute the `run()` function, +// catching any errors and printing them to `stderr` before exiting the +// process. +#[async_std::main] +async fn main() { + if let Err(e) = run().await { + eprintln!("Application error: {}", e); + process::exit(1); + } +} diff --git a/examples/ssb-client.rs b/examples/ssb-client.rs deleted file mode 100644 index a5c0a3b..0000000 --- a/examples/ssb-client.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::process; - -use golgi::error::GolgiError; -use golgi::messages::SsbMessageContent; -use golgi::sbot::Sbot; - -async fn run() -> Result<(), GolgiError> { - let mut sbot_client = Sbot::connect(None, None).await?; - - let id = sbot_client.whoami().await?; - println!("whoami: {}", id); - - let name = SsbMessageContent::About { - about: id.clone(), - name: Some("golgi".to_string()), - title: None, - branch: None, - image: None, - description: None, - location: None, - start_datetime: None, - }; - - let name_msg_ref = sbot_client.publish(name).await?; - println!("name_msg_ref: {}", name_msg_ref); - - let post = SsbMessageContent::Post { - text: "golgi go womp womp".to_string(), - mentions: None, - }; - - let post_msg_ref = sbot_client.publish(post).await?; - println!("post_msg_ref: {}", post_msg_ref); - - let post_msg_ref = sbot_client - .publish_description("this is a description7") - .await?; - println!("description: {}", post_msg_ref); - - let author: String = id.clone(); - println!("author: {:?}", author); - let description = sbot_client.get_description(&author).await?; - println!("found description: {:?}", description); - - let post = SsbMessageContent::Post { - text: "golgi go womp womp2".to_string(), - mentions: None, - }; - - let post_msg_ref = sbot_client.publish(post).await?; - println!("post_msg_ref2: {}", post_msg_ref); - - Ok(()) -} - -#[async_std::main] -async fn main() { - if let Err(e) = run().await { - eprintln!("Application error: {}", e); - process::exit(1); - } -} diff --git a/examples/ssb-friends.rs b/examples/ssb-friends.rs deleted file mode 100644 index 68b382c..0000000 --- a/examples/ssb-friends.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::process; - -use golgi::error::GolgiError; -use golgi::sbot::Sbot; -use golgi::sbot::{FriendsHops, RelationshipQuery}; - -async fn run() -> Result<(), GolgiError> { - let mut sbot_client = Sbot::connect(None, None).await?; - - let id = sbot_client.whoami().await?; - println!("whoami: {}", id); - - // test ids to follow and block - let to_follow = String::from("@5Pt3dKy2HTJ0mWuS78oIiklIX0gBz6BTfEnXsbvke9c=.ed25519"); - let to_block = String::from("@7Y4nwfQmVtAilEzi5knXdS2gilW7cGKSHXdXoT086LM=.ed25519"); - - // follow to_follow - let response = sbot_client - .set_relationship(&to_follow, true, false) - .await?; - println!("follow_response: {:?}", response); - - // block to_block - let response = sbot_client.set_relationship(&to_block, false, true).await?; - println!("follow_response: {:?}", response); - - // print all users you are following - let follows = sbot_client - .friends_hops(FriendsHops { - max: 1, - start: None, - // doesnt seem like reverse does anything, currently - reverse: Some(false), - }) - .await?; - println!("follows: {:?}", follows); - - // print if you are following to_follow (should be true) - let mref = sbot_client - .friends_is_following(RelationshipQuery { - source: id.clone(), - dest: to_follow.clone(), - }) - .await?; - println!("isfollowingmref: {}", mref); - - // print if you are blocking to_block (should be true) - let mref = sbot_client - .friends_is_blocking(RelationshipQuery { - source: id.clone(), - dest: to_block.clone(), - }) - .await?; - println!("isblockingmref: {}", mref); - - // print if you are blocking to_follow (should be false) - let mref = sbot_client - .friends_is_blocking(RelationshipQuery { - source: id.clone(), - dest: to_follow, - }) - .await?; - println!("isblockingmref(should be false): {}", mref); - - // print if you are following to_block (should be false) - let mref = sbot_client - .friends_is_following(RelationshipQuery { - source: id, - dest: to_block.clone(), - }) - .await?; - println!("isfollowingmref(should be false): {}", mref); - - Ok(()) -} - -#[async_std::main] -async fn main() { - if let Err(e) = run().await { - eprintln!("Application error: {}", e); - process::exit(1); - } -} diff --git a/examples/ssb-invite.rs b/examples/ssb-invite.rs deleted file mode 100644 index 17db7c8..0000000 --- a/examples/ssb-invite.rs +++ /dev/null @@ -1,47 +0,0 @@ -use std::process; - -use kuska_ssb::api::dto::content::PubAddress; - -use golgi::error::GolgiError; -use golgi::messages::SsbMessageContent; -use golgi::sbot::Sbot; - -async fn run() -> Result<(), GolgiError> { - let mut sbot_client = Sbot::connect(None, None).await?; - - let id = sbot_client.whoami().await?; - println!("whoami: {}", id); - - // publish a pub address message (in order to test accepting invite from other client) - let pub_address_msg = SsbMessageContent::Pub { - address: Some(PubAddress { - host: Some("127.0.0.1".to_string()), - port: 8009, - key: id, - }), - }; - - let pub_msg_ref = sbot_client.publish(pub_address_msg).await?; - println!("pub_msg_ref: {}", pub_msg_ref); - - let invite_code = sbot_client.invite_create(1).await?; - println!("invite (1 use): {:?}", invite_code); - - let invite_code_2 = sbot_client.invite_create(7).await?; - println!("invite (7 uses): {:?}", invite_code_2); - - let test_invite = - "net:ssbroom2.commoninternet.net:8009~shs:wm8a1zHWjtESv4XSKMWU/rPRhnAoAiSAe4hQSY0UF5A="; - let mref = sbot_client.invite_use(test_invite).await?; - println!("mref: {:?}", mref); - - Ok(()) -} - -#[async_std::main] -async fn main() { - if let Err(e) = run().await { - eprintln!("Application error: {}", e); - process::exit(1); - } -} diff --git a/examples/ssb-stream-example.rs b/examples/ssb-stream-example.rs deleted file mode 100644 index cf4a223..0000000 --- a/examples/ssb-stream-example.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::process; - -use async_std::stream::StreamExt; -use futures::{pin_mut, TryStreamExt}; - -use golgi::error::GolgiError; -use golgi::messages::{SsbMessageContentType, SsbMessageValue}; -use golgi::sbot::Sbot; - -async fn run() -> Result<(), GolgiError> { - let mut sbot_client = Sbot::connect(None, None).await?; - - let id = sbot_client.whoami().await?; - println!("whoami: {}", id); - - let author = id.clone(); - - // create a history stream - let history_stream = sbot_client - .create_history_stream(author.to_string()) - .await?; - - // loop through the results until the end of the stream - pin_mut!(history_stream); // needed for iteration - println!("looping through stream"); - while let Some(res) = history_stream.next().await { - match res { - Ok(value) => { - println!("value: {:?}", value); - } - Err(err) => { - println!("err: {:?}", err); - } - } - } - println!("reached end of stream"); - - // create a history stream and convert it into a Vec using try_collect - // (if there is any error in the results, it will be raised) - let history_stream = sbot_client - .create_history_stream(author.to_string()) - .await?; - let results: Vec = history_stream.try_collect().await?; - for x in results { - println!("x: {:?}", x); - } - - // example to create a history stream and use a map to convert stream of SsbMessageValue - // into a stream of tuples of (String, SsbMessageContentType) - let history_stream = sbot_client - .create_history_stream(author.to_string()) - .await?; - let type_stream = history_stream.map(|msg| match msg { - Ok(val) => { - let message_type = val.get_message_type()?; - let tuple: (String, SsbMessageContentType) = (val.signature, message_type); - Ok(tuple) - } - Err(err) => Err(err), - }); - pin_mut!(type_stream); // needed for iteration - println!("looping through type stream"); - while let Some(res) = type_stream.next().await { - match res { - Ok(value) => { - println!("value: {:?}", value); - } - Err(err) => { - println!("err: {:?}", err); - } - } - } - println!("reached end of type stream"); - - // return Ok - Ok(()) -} - -#[async_std::main] -async fn main() { - if let Err(e) = run().await { - eprintln!("Application error: {}", e); - process::exit(1); - } -} diff --git a/examples/streams.rs b/examples/streams.rs new file mode 100644 index 0000000..8d05342 --- /dev/null +++ b/examples/streams.rs @@ -0,0 +1,125 @@ +use std::process; + +use async_std::stream::StreamExt; +use futures::TryStreamExt; + +use golgi::{ + messages::{SsbMessageContentType, SsbMessageValue}, + GolgiError, Sbot, +}; + +// Golgi is an asynchronous library so we must call it from within an +// async function. The `GolgiError` type encapsulates all possible +// error variants for the library. +async fn run() -> Result<(), GolgiError> { + // Attempt to connect to an sbot instance using the default IP address, + // port and network key (aka. capabilities key). + let mut sbot_client = Sbot::init(None, None).await?; + + // Call the `whoami` RPC method to retrieve the public key for the sbot + // identity. This is our 'local' public key. + let id = sbot_client.whoami().await?; + + // Print the public key (identity) to `stdout`. + println!("whoami: {}", id); + + let author = id.clone(); + + // Create an ordered stream of all messages authored by the `author` + // identity. + let history_stream = sbot_client + .create_history_stream(author.to_string()) + .await?; + + // Pin the stream to the stack to allow polling of the `future`. + futures::pin_mut!(history_stream); + + println!("looping through stream"); + + // Iterate through each element in the stream and match on the `Result`. + // In this case, each element has type `Result`. + while let Some(res) = history_stream.next().await { + match res { + Ok(value) => { + // Print the `SsbMessageValue` of this element to `stdout`. + println!("value: {:?}", value); + } + Err(err) => { + // Print the `GolgiError` of this element to `stderr`. + eprintln!("err: {:?}", err); + } + } + } + + println!("reached end of stream"); + + // Create an ordered stream of all messages authored by the `author` + // identity. + let history_stream = sbot_client + .create_history_stream(author.to_string()) + .await?; + + // Collect the stream elements into a `Vec` using + // `try_collect`. A `GolgiError` will be returned from the `run` + // function if any element contains an error. + let results: Vec = history_stream.try_collect().await?; + + // Loop through the `SsbMessageValue` elements, printing each one + // to `stdout`. + for x in results { + println!("x: {:?}", x); + } + + // Create an ordered stream of all messages authored by the `author` + // identity. + let history_stream = sbot_client + .create_history_stream(author.to_string()) + .await?; + + // Iterate through the elements in the stream and use `map` to convert + // each `SsbMessageValue` element into a tuple of + // `(String, SsbMessageContentType)`. This is an example of stream + // conversion. + let type_stream = history_stream.map(|msg| match msg { + Ok(val) => { + let message_type = val.get_message_type()?; + let tuple: (String, SsbMessageContentType) = (val.signature, message_type); + Ok(tuple) + } + Err(err) => Err(err), + }); + + // Pin the stream to the stack to allow polling of the `future`. + futures::pin_mut!(type_stream); + + println!("looping through type stream"); + + // Iterate through each element in the stream and match on the `Result`. + // In this case, each element has type + // `Result<(String, SsbMessageContentType), GolgiError>`. + while let Some(res) = type_stream.next().await { + match res { + Ok(value) => { + println!("value: {:?}", value); + } + Err(err) => { + println!("err: {:?}", err); + } + } + } + + println!("reached end of type stream"); + + Ok(()) +} + +// Enable an async main function and execute the `run()` function, +// catching any errors and printing them to `stderr` before exiting the +// process. +#[async_std::main] +async fn main() { + if let Err(e) = run().await { + eprintln!("Application error: {}", e); + process::exit(1); + } +} diff --git a/src/api/about.rs b/src/api/about.rs new file mode 100644 index 0000000..328b803 --- /dev/null +++ b/src/api/about.rs @@ -0,0 +1,385 @@ +//! Retrieve data about a peer. +//! +//! Implements the following methods: +//! +//! - [`Sbot::get_about_info`] +//! - [`Sbot::get_about_message_stream`] +//! - [`Sbot::get_description`] +//! - [`Sbot::get_latest_about_message`] +//! - [`Sbot::get_name`] +//! - [`Sbot::get_name_and_image`] +//! - [`Sbot::get_profile_info`] + +use std::collections::HashMap; + +use async_std::stream::{Stream, StreamExt}; + +use crate::{ + api::get_subset::{SubsetQuery, SubsetQueryOptions}, + error::GolgiError, + messages::{SsbMessageContentType, SsbMessageValue}, + sbot::Sbot, +}; + +impl Sbot { + /// Get all the `about` type messages for a peer in order of recency + /// (ie. most recent messages first). + /// + /// # Example + /// + /// ```rust + /// use async_std::stream::{Stream, StreamExt}; + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn about_message_stream() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519"; + /// + /// let about_message_stream = sbot_client.get_about_message_stream(ssb_id).await?; + /// + /// // Make the stream into an iterator. + /// futures::pin_mut!(about_message_stream); + /// + /// about_message_stream.for_each(|msg| { + /// match msg { + /// Ok(val) => println!("msg value: {:?}", val), + /// Err(e) => eprintln!("error: {}", e), + /// } + /// }).await; + /// + /// Ok(()) + /// } + /// ``` + pub async fn get_about_message_stream( + &mut self, + ssb_id: &str, + ) -> Result>, GolgiError> { + let query = SubsetQuery::Author { + op: "author".to_string(), + feed: ssb_id.to_string(), + }; + + // specify that most recent messages should be returned first + let query_options = SubsetQueryOptions { + descending: Some(true), + keys: None, + page_limit: None, + }; + + let get_subset_stream = self.get_subset_stream(query, Some(query_options)).await?; + + // TODO: after fixing sbot regression, + // change this subset query to filter by type about in addition to author + // and remove this filter section + // filter down to about messages + let about_message_stream = get_subset_stream.filter(|msg| match msg { + Ok(val) => val.is_message_type(SsbMessageContentType::About), + Err(_err) => false, + }); + + // return about message stream + Ok(about_message_stream) + } + + /// Get the value of the latest `about` type message, containing the given + /// `key`, for a peer. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn name_info() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519"; + /// let key = "name"; + /// + /// let name_info = sbot_client.get_latest_about_message(ssb_id, key).await?; + /// + /// match name_info { + /// Some(name) => println!("peer {} is named {}", ssb_id, name), + /// None => println!("no name found for peer {}", ssb_id) + /// } + /// + /// Ok(()) + /// } + /// ``` + pub async fn get_latest_about_message( + &mut self, + ssb_id: &str, + key: &str, + ) -> Result, GolgiError> { + // get about_message_stream + let about_message_stream = self.get_about_message_stream(ssb_id).await?; + + // now we have a stream of about messages with most recent at the front + // of the vector + futures::pin_mut!(about_message_stream); + + // iterate through the vector looking for most recent about message with + // the given key + let latest_about_message_res: Option> = + about_message_stream + // find the first msg that contains the field `key` + .find(|res| match res { + Ok(msg) => msg.content.get(key).is_some(), + Err(_) => false, + }) + .await; + + // Option> -> Option + let latest_about_message = latest_about_message_res.and_then(|msg| msg.ok()); + + // Option -> Option + let latest_about_value = latest_about_message.and_then(|msg| { + msg + // SsbMessageValue -> Option<&Value> + .content + .get(key) + // Option<&Value> -> + .and_then(|value| value.as_str()) + // Option<&str> -> Option + .map(|value| value.to_string()) + }); + + // return value is either `Ok(Some(String))` or `Ok(None)` + Ok(latest_about_value) + } + + /// Get the latest `name`, `description` and `image` values for a peer, + /// as defined in their `about` type messages. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn profile_info() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519"; + /// + /// let profile_info = sbot_client.get_profile_info(ssb_id).await?; + /// + /// let name = profile_info.get("name"); + /// let description = profile_info.get("description"); + /// let image = profile_info.get("image"); + /// + /// match (name, description, image) { + /// (Some(name), Some(desc), Some(image)) => { + /// println!( + /// "peer {} is named {}. their profile image blob reference is {} and they describe themself as follows: {}", + /// ssb_id, name, image, desc, + /// ) + /// }, + /// (_, _, _) => { + /// eprintln!("failed to retrieve all profile info values") + /// } + /// } + /// + /// Ok(()) + /// } + /// ``` + pub async fn get_profile_info( + &mut self, + ssb_id: &str, + ) -> Result, GolgiError> { + let keys_to_search_for = vec!["name", "description", "image"]; + self.get_about_info(ssb_id, keys_to_search_for).await + } + + /// Get the latest `name` and `image` values for a peer. This method can + /// be used to display profile images of a list of users. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn name_and_image_info() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519"; + /// + /// let profile_info = sbot_client.get_name_and_image(ssb_id).await?; + /// + /// let name = profile_info.get("name"); + /// let image = profile_info.get("image"); + /// + /// match (name, image) { + /// (Some(name), Some(image)) => { + /// println!( + /// "peer {} is named {}. their profile image blob reference is {}.", + /// ssb_id, name, image, + /// ) + /// }, + /// (Some(name), None) => { + /// println!( + /// "peer {} is named {}. no image blob reference was found for them.", + /// ssb_id, name, + /// ) + /// }, + /// (_, _) => { + /// eprintln!("failed to retrieve all profile info values") + /// } + /// } + /// + /// Ok(()) + /// } + pub async fn get_name_and_image( + &mut self, + ssb_id: &str, + ) -> Result, GolgiError> { + let keys_to_search_for = vec!["name", "image"]; + self.get_about_info(ssb_id, keys_to_search_for).await + } + + /// Get the latest values for the provided keys from the `about` type + /// messages of a peer. The method will return once a value has been + /// found for each key, or once all messages have been checked. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn about_info() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519"; + /// let keys_to_search_for = vec!["name", "description"]; + /// + /// let about_info = sbot_client.get_about_info(ssb_id, keys_to_search_for).await?; + /// + /// let name = about_info.get("name"); + /// let description = about_info.get("description"); + /// + /// match (name, description) { + /// (Some(name), Some(desc)) => { + /// println!( + /// "peer {} is named {}. they describe themself as: {}", + /// ssb_id, name, desc, + /// ) + /// }, + /// (Some(name), None) => { + /// println!( + /// "peer {} is named {}. no description was found for them.", + /// ssb_id, name, + /// ) + /// }, + /// (_, _) => { + /// eprintln!("failed to retrieve all profile info values") + /// } + /// } + /// + /// Ok(()) + /// } + /// ``` + pub async fn get_about_info( + &mut self, + ssb_id: &str, + mut keys_to_search_for: Vec<&str>, + ) -> Result, GolgiError> { + // get about_message_stream + let about_message_stream = self.get_about_message_stream(ssb_id).await?; + + // now we have a stream of about messages with most recent at the front + // of the vector + + // `pin_mut!` is needed for iteration + futures::pin_mut!(about_message_stream); + + let mut profile_info: HashMap = HashMap::new(); + + // iterate through the stream while it still has more values and + // we still have keys we are looking for + while let Some(res) = about_message_stream.next().await { + // if there are no more keys we are looking for, then we are done + if keys_to_search_for.is_empty() { + break; + } + // if there are still keys we are looking for, then continue searching + match res { + Ok(msg) => { + // for each key we are searching for, check if this about + // message contains a value for that key + for key in &keys_to_search_for.clone() { + let option_val = msg + .content + .get(key) + .and_then(|val| val.as_str()) + .map(|val| val.to_string()); + match option_val { + Some(val) => { + // if a value is found, then insert it + profile_info.insert(key.to_string(), val); + // remove this key from keys_to_search_for, + // since we are no longer searching for it + keys_to_search_for.retain(|val| val != key) + } + None => continue, + } + } + } + Err(_err) => { + // skip errors + continue; + } + } + } + + Ok(profile_info) + } + + /// Get the latest `name` value for a peer. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn name_info() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519"; + /// + /// if let Some(name) = sbot_client.get_name(ssb_id).await? { + /// println!("peer {} is named {}", ssb_id, name) + /// } else { + /// eprintln!("no name found for peer {}", ssb_id) + /// } + /// + /// Ok(()) + /// } + /// ``` + pub async fn get_name(&mut self, ssb_id: &str) -> Result, GolgiError> { + self.get_latest_about_message(ssb_id, "name").await + } + + /// Get the latest `description` value for a peer. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn description_info() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519"; + /// + /// if let Some(desc) = sbot_client.get_description(ssb_id).await? { + /// println!("peer {} describes themself as follows: {}", ssb_id, desc) + /// } else { + /// eprintln!("no description found for peer {}", ssb_id) + /// } + /// + /// Ok(()) + /// } + /// ``` + pub async fn get_description(&mut self, ssb_id: &str) -> Result, GolgiError> { + self.get_latest_about_message(ssb_id, "description").await + } +} diff --git a/src/api/friends.rs b/src/api/friends.rs new file mode 100644 index 0000000..63c73af --- /dev/null +++ b/src/api/friends.rs @@ -0,0 +1,326 @@ +//! Define peer relationships and query the social graph. +//! +//! Implements the following methods: +//! +//! - [`Sbot::block`] +//! - [`Sbot::follow`] +//! - [`Sbot::friends_hops`] +//! - [`Sbot::friends_is_blocking`] +//! - [`Sbot::friends_is_following`] +//! - [`Sbot::get_follows`] +//! - [`Sbot::set_relationship`] + +use crate::{error::GolgiError, messages::SsbMessageContent, sbot::Sbot, utils}; + +// re-export friends-related kuska types +pub use kuska_ssb::api::dto::content::{FriendsHops, RelationshipQuery}; + +impl Sbot { + /// Follow a peer. + /// + /// This is a convenience method to publish a contact message with + /// following: `true` and blocking: `false`. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn follow_peer() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519"; + /// + /// match sbot_client.follow(ssb_id).await { + /// Ok(msg_ref) => { + /// println!("follow msg reference is: {}", msg_ref) + /// }, + /// Err(e) => eprintln!("failed to follow {}: {}", ssb_id, e) + /// } + /// + /// Ok(()) + /// } + /// ``` + pub async fn follow(&mut self, contact: &str) -> Result { + self.set_relationship(contact, true, false).await + } + + /// Block a peer. + /// + /// This is a convenience method to publish a contact message with + /// following: `false` and blocking: `true`. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn block_peer() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519"; + /// + /// match sbot_client.block(ssb_id).await { + /// Ok(msg_ref) => { + /// println!("block msg reference is: {}", msg_ref) + /// }, + /// Err(e) => eprintln!("failed to block {}: {}", ssb_id, e) + /// } + /// + /// Ok(()) + /// } + /// ``` + pub async fn block(&mut self, contact: &str) -> Result { + self.set_relationship(contact, false, true).await + } + + /// Publish a contact message defining the relationship for a peer. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn relationship() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519"; + /// let following = true; + /// let blocking = false; + /// + /// match sbot_client.set_relationship(ssb_id, following, blocking).await { + /// Ok(msg_ref) => { + /// println!("contact msg reference is: {}", msg_ref) + /// }, + /// Err(e) => eprintln!("failed to set relationship for {}: {}", ssb_id, e) + /// } + /// + /// Ok(()) + /// } + /// ``` + pub async fn set_relationship( + &mut self, + contact: &str, + following: bool, + blocking: bool, + ) -> Result { + let msg = SsbMessageContent::Contact { + contact: Some(contact.to_string()), + following: Some(following), + blocking: Some(blocking), + autofollow: None, + }; + self.publish(msg).await + } + + /// Get the follow status of two peers (ie. does one peer follow the other?). + /// + /// A `RelationshipQuery` `struct` must be defined and passed into this method. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError, api::friends::RelationshipQuery}; + /// + /// async fn relationship() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let peer_a = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519"; + /// let peer_b = "@3QoWCcy46X9a4jTnOl8m3+n1gKfbsukWuODDxNGN0W8=.ed25519"; + /// + /// let query = RelationshipQuery { + /// source: peer_a.to_string(), + /// dest: peer_b.to_string(), + /// }; + /// + /// match sbot_client.friends_is_following(query).await { + /// Ok(following) if following == "true" => { + /// println!("{} is following {}", peer_a, peer_b) + /// }, + /// Ok(_) => println!("{} is not following {}", peer_a, peer_b), + /// Err(e) => eprintln!("failed to query relationship status for {} and {}: {}", peer_a, + /// peer_b, e) + /// } + /// + /// Ok(()) + /// } + /// ``` + pub async fn friends_is_following( + &mut self, + args: RelationshipQuery, + ) -> Result { + let mut sbot_connection = self.get_sbot_connection().await?; + let req_id = sbot_connection + .client + .friends_is_following_req_send(args) + .await?; + + utils::get_async( + &mut sbot_connection.rpc_reader, + req_id, + utils::string_res_parse, + ) + .await + } + + /// Get the block status of two peers (ie. does one peer block the other?). + /// + /// A `RelationshipQuery` `struct` must be defined and passed into this method. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError, api::friends::RelationshipQuery}; + /// + /// async fn relationship() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let peer_a = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519"; + /// let peer_b = "@3QoWCcy46X9a4jTnOl8m3+n1gKfbsukWuODDxNGN0W8=.ed25519"; + /// + /// let query = RelationshipQuery { + /// source: peer_a.to_string(), + /// dest: peer_b.to_string(), + /// }; + /// + /// match sbot_client.friends_is_blocking(query).await { + /// Ok(blocking) if blocking == "true" => { + /// println!("{} is blocking {}", peer_a, peer_b) + /// }, + /// Ok(_) => println!("{} is not blocking {}", peer_a, peer_b), + /// Err(e) => eprintln!("failed to query relationship status for {} and {}: {}", peer_a, + /// peer_b, e) + /// } + /// + /// Ok(()) + /// } + /// ``` + pub async fn friends_is_blocking( + &mut self, + args: RelationshipQuery, + ) -> Result { + let mut sbot_connection = self.get_sbot_connection().await?; + let req_id = sbot_connection + .client + .friends_is_blocking_req_send(args) + .await?; + + utils::get_async( + &mut sbot_connection.rpc_reader, + req_id, + utils::string_res_parse, + ) + .await + } + + /// Get a list of peers followed by the local peer. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn follows() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let follows = sbot_client.get_follows().await?; + /// + /// if follows.is_empty() { + /// println!("we do not follow any peers") + /// } else { + /// follows.iter().for_each(|peer| println!("we follow {}", peer)) + /// } + /// + /// Ok(()) + /// } + /// ``` + pub async fn get_follows(&mut self) -> Result, GolgiError> { + self.friends_hops(FriendsHops { + max: 1, + start: None, + reverse: Some(false), + }) + .await + } + + /// Get a list of peers following the local peer. + /// + /// NOTE: this method is not currently working as expected. + /// + /// go-sbot does not currently implement the `reverse=True` parameter. + /// As a result, the parameter is ignored and this method returns follows + /// instead of followers. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn followers() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// // let followers = sbot_client.get_followers().await?; + /// + /// // if followers.is_empty() { + /// // println!("no peers follow us") + /// // } else { + /// // followers.iter().for_each(|peer| println!("{} is following us", peer)) + /// // } + /// + /// Ok(()) + /// } + /// ``` + async fn _get_followers(&mut self) -> Result, GolgiError> { + self.friends_hops(FriendsHops { + max: 1, + start: None, + reverse: Some(true), + }) + .await + } + + /// Get a list of peers within the specified hops range. + /// + /// A `RelationshipQuery` `struct` must be defined and passed into this method. + /// + /// When opts.reverse = True, it should return peers who are following you + /// (but this is not currently working). + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError, api::friends::FriendsHops}; + /// + /// async fn peers_within_range() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let hops = 2; + /// + /// let query = FriendsHops { + /// max: hops, + /// reverse: Some(false), + /// start: None, + /// }; + /// + /// let peers = sbot_client.friends_hops(query).await?; + /// + /// if peers.is_empty() { + /// println!("no peers found within {} hops", hops) + /// } else { + /// peers.iter().for_each(|peer| println!("{} is within {} hops", peer, hops)) + /// } + /// + /// Ok(()) + /// } + /// ``` + pub async fn friends_hops(&mut self, args: FriendsHops) -> Result, GolgiError> { + let mut sbot_connection = self.get_sbot_connection().await?; + let req_id = sbot_connection.client.friends_hops_req_send(args).await?; + utils::get_source_until_eof( + &mut sbot_connection.rpc_reader, + req_id, + utils::string_res_parse, + ) + .await + } +} diff --git a/src/api/get_subset.rs b/src/api/get_subset.rs new file mode 100644 index 0000000..8461835 --- /dev/null +++ b/src/api/get_subset.rs @@ -0,0 +1,86 @@ +//! Perform subset queries. +//! +//! Implements the following methods: +//! +//! - [`Sbot::get_subset_stream`] + +use async_std::stream::Stream; + +use crate::{error::GolgiError, messages::SsbMessageValue, sbot::Sbot, utils}; + +// re-export subset-related kuska types +pub use kuska_ssb::api::dto::content::{SubsetQuery, SubsetQueryOptions}; + +impl Sbot { + /// Make a subset query, as defined by the [Subset replication for SSB specification](https://github.com/ssb-ngi-pointer/ssb-subset-replication-spec). + /// + /// Calls the `partialReplication. getSubset` RPC method. + /// + /// # Arguments + /// + /// * `query` - A `SubsetQuery` which specifies the desired query filters. + /// * `options` - An Option<`SubsetQueryOptions`> which, if provided, adds + /// additional specifications to the query, such as page limit and/or + /// descending results. + /// + /// # Example + /// + /// ```rust + /// use async_std::stream::StreamExt; + /// use golgi::{ + /// Sbot, + /// GolgiError, + /// api::get_subset::{ + /// SubsetQuery, + /// SubsetQueryOptions + /// } + /// }; + /// + /// async fn query() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let post_query = SubsetQuery::Type { + /// op: "type".to_string(), + /// string: "post".to_string() + /// }; + /// + /// let post_query_opts = SubsetQueryOptions { + /// descending: Some(true), + /// keys: None, + /// page_limit: Some(5), + /// }; + /// + /// // Return 5 `post` type messages from any author in descending order. + /// let query_stream = sbot_client + /// .get_subset_stream(post_query, Some(post_query_opts)) + /// .await?; + /// + /// // Iterate over the stream and pretty-print each returned message + /// // value while ignoring any errors. + /// query_stream.for_each(|msg| match msg { + /// Ok(val) => println!("{:#?}", val), + /// Err(_) => (), + /// }); + /// + /// Ok(()) + /// } + /// ``` + pub async fn get_subset_stream( + &mut self, + query: SubsetQuery, + options: Option, + ) -> Result>, GolgiError> { + let mut sbot_connection = self.get_sbot_connection().await?; + let req_id = sbot_connection + .client + .getsubset_req_send(query, options) + .await?; + let get_subset_stream = utils::get_source_stream( + sbot_connection.rpc_reader, + req_id, + utils::ssb_message_res_parse, + ) + .await; + Ok(get_subset_stream) + } +} diff --git a/src/api/history_stream.rs b/src/api/history_stream.rs new file mode 100644 index 0000000..e59316e --- /dev/null +++ b/src/api/history_stream.rs @@ -0,0 +1,63 @@ +//! Return a history stream. +//! +//! Implements the following methods: +//! +//! - [`Sbot::create_history_stream`] + +use async_std::stream::Stream; +use kuska_ssb::api::dto::CreateHistoryStreamIn; + +use crate::{error::GolgiError, messages::SsbMessageValue, sbot::Sbot, utils}; + +impl Sbot { + /// Call the `createHistoryStream` RPC method. + /// + /// # Example + /// + /// ```rust + /// use async_std::stream::StreamExt; + /// use golgi::{ + /// Sbot, + /// GolgiError, + /// api::get_subset::{ + /// SubsetQuery, + /// SubsetQueryOptions + /// } + /// }; + /// + /// async fn history() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519".to_string(); + /// + /// let history_stream = sbot_client.create_history_stream(ssb_id).await?; + /// + /// history_stream.for_each(|msg| { + /// match msg { + /// Ok(val) => println!("msg value: {:?}", val), + /// Err(e) => eprintln!("error: {}", e), + /// } + /// }).await; + /// + /// Ok(()) + /// } + /// ``` + pub async fn create_history_stream( + &mut self, + id: String, + ) -> Result>, GolgiError> { + let mut sbot_connection = self.get_sbot_connection().await?; + let args = CreateHistoryStreamIn::new(id); + let req_id = sbot_connection + .client + .create_history_stream_req_send(&args) + .await?; + let history_stream = utils::get_source_stream( + sbot_connection.rpc_reader, + req_id, + utils::ssb_message_res_parse, + ) + .await; + Ok(history_stream) + } +} diff --git a/src/api/invite.rs b/src/api/invite.rs new file mode 100644 index 0000000..f9379f0 --- /dev/null +++ b/src/api/invite.rs @@ -0,0 +1,79 @@ +//! Create and use invite codes. +//! +//! Implements the following methods: +//! +//! - [`Sbot::invite_create`] +//! - [`Sbot::invite_use`] + +use crate::{error::GolgiError, sbot::Sbot, utils}; + +impl Sbot { + /// Generate an invite code. + /// + /// Calls the `invite.create` RPC method and returns the code. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn invite_code_generator() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let invite_code = sbot_client.invite_create(5).await?; + /// + /// println!("this invite code can be used 5 times: {}", invite_code); + /// + /// Ok(()) + /// } + /// ``` + pub async fn invite_create(&mut self, uses: u16) -> Result { + let mut sbot_connection = self.get_sbot_connection().await?; + let req_id = sbot_connection.client.invite_create_req_send(uses).await?; + + utils::get_async( + &mut sbot_connection.rpc_reader, + req_id, + utils::string_res_parse, + ) + .await + } + + /// Use an invite code. + /// + /// Calls the `invite.use` RPC method and returns a reference to the follow + /// message. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn invite_code_consumer() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let invite_code = "127.0.0.1:8008:@0iMa+vP7B2aMrV3dzRxlch/iqZn/UM3S3Oo2oVeILY8=.ed25519~ZHNjeajPB/84NjjsrglZInlh46W55RcNDPcffTPgX/Q="; + /// + /// match sbot_client.invite_use(invite_code).await { + /// Ok(msg_ref) => println!("consumed invite code. msg reference: {}", msg_ref), + /// Err(e) => eprintln!("failed to consume the invite code: {}", e), + /// } + /// + /// Ok(()) + /// } + /// ``` + pub async fn invite_use(&mut self, invite_code: &str) -> Result { + let mut sbot_connection = self.get_sbot_connection().await?; + let req_id = sbot_connection + .client + .invite_use_req_send(invite_code) + .await?; + + utils::get_async( + &mut sbot_connection.rpc_reader, + req_id, + utils::string_res_parse, + ) + .await + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..ef9ba50 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,10 @@ +//! API for interacting with a running go-sbot instance. +pub mod about; +pub mod friends; +pub mod get_subset; +pub mod history_stream; +pub mod invite; +pub mod publish; +pub mod whoami; + +pub use crate::sbot::*; diff --git a/src/api/publish.rs b/src/api/publish.rs new file mode 100644 index 0000000..33c8361 --- /dev/null +++ b/src/api/publish.rs @@ -0,0 +1,154 @@ +//! Publish Scuttlebutt messages. +//! +//! Implements the following methods: +//! +//! - [`Sbot::publish`] +//! - [`Sbot::publish_description`] +//! - [`Sbot::publish_name`] +//! - [`Sbot::publish_post`] + +use crate::{error::GolgiError, messages::SsbMessageContent, sbot::Sbot, utils}; + +impl Sbot { + /// Publish a message. + /// + /// # Arguments + /// + /// * `msg` - A `SsbMessageContent` `enum` whose variants include `Pub`, + /// `Post`, `Contact`, `About`, `Channel` and `Vote`. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError, messages::SsbMessageContent}; + /// + /// async fn publish_a_msg() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// // Construct an SSB message of type `post`. + /// let post = SsbMessageContent::Post { + /// text: "And then those vesicles, filled with the Golgi products, move to the rest of the cell".to_string(), + /// mentions: None, + /// }; + /// + /// let msg_ref = sbot_client.publish(post).await?; + /// + /// println!("msg reference for the golgi post: {}", msg_ref); + /// + /// Ok(()) + /// } + /// ``` + pub async fn publish(&mut self, msg: SsbMessageContent) -> Result { + let mut sbot_connection = self.get_sbot_connection().await?; + let req_id = sbot_connection.client.publish_req_send(msg).await?; + + utils::get_async( + &mut sbot_connection.rpc_reader, + req_id, + utils::string_res_parse, + ) + .await + } + + /// Publish a post. + /// + /// Convenient wrapper around the `publish` method which constructs and + /// publishes a `post` type message appropriately from a string. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn publish_a_post() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let text = "The Golgi is located right near the nucleus."; + /// + /// let msg_ref = sbot_client.publish_post(text).await?; + /// + /// println!("msg reference for the golgi post: {}", msg_ref); + /// + /// Ok(()) + /// } + /// ``` + pub async fn publish_post(&mut self, text: &str) -> Result { + let msg = SsbMessageContent::Post { + text: text.to_string(), + mentions: None, + }; + self.publish(msg).await + } + + /// Publish a description for the local identity. + /// + /// Convenient wrapper around the `publish` method which constructs and + /// publishes an `about` type description message appropriately from a string. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn publish_a_description() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let description = "The Golgi apparatus was identified by the Italian scientist Camillo Golgi in 1897."; + /// + /// let msg_ref = sbot_client.publish_description(description).await?; + /// + /// println!("msg reference for the golgi description: {}", msg_ref); + /// + /// Ok(()) + /// } + /// ``` + pub async fn publish_description(&mut self, description: &str) -> Result { + let msg = SsbMessageContent::About { + about: self.id.to_string(), + name: None, + title: None, + branch: None, + image: None, + description: Some(description.to_string()), + location: None, + start_datetime: None, + }; + self.publish(msg).await + } + + /// Publish a name for the local identity. + /// + /// Convenient wrapper around the `publish` method which constructs and + /// publishes an `about` type name message appropriately from a string. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn publish_a_name() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let name = "glyphski_golgionikus"; + /// + /// let msg_ref = sbot_client.publish_name(name).await?; + /// + /// println!("msg reference: {}", msg_ref); + /// + /// Ok(()) + /// } + /// ``` + pub async fn publish_name(&mut self, name: &str) -> Result { + let msg = SsbMessageContent::About { + about: self.id.to_string(), + name: Some(name.to_string()), + title: None, + branch: None, + image: None, + description: None, + location: None, + start_datetime: None, + }; + self.publish(msg).await + } +} diff --git a/src/api/whoami.rs b/src/api/whoami.rs new file mode 100644 index 0000000..8e29871 --- /dev/null +++ b/src/api/whoami.rs @@ -0,0 +1,45 @@ +//! Return the SSB ID of the local sbot instance. +//! +//! Implements the following methods: +//! +//! - [`Sbot::whoami`] + +use crate::{error::GolgiError, sbot::Sbot, utils}; + +impl Sbot { + /// Get the public key of the local identity. + /// + /// # Example + /// + /// ```rust + /// use golgi::{Sbot, GolgiError}; + /// + /// async fn fetch_id() -> Result<(), GolgiError> { + /// let mut sbot_client = Sbot::init(None, None).await?; + /// + /// let pub_key = sbot_client.whoami().await?; + /// + /// println!("local ssb id: {}", pub_key); + /// + /// Ok(()) + /// } + /// ``` + pub async fn whoami(&mut self) -> Result { + let mut sbot_connection = self.get_sbot_connection().await?; + let req_id = sbot_connection.client.whoami_req_send().await?; + + let result = utils::get_async( + &mut sbot_connection.rpc_reader, + req_id, + utils::json_res_parse, + ) + .await?; + + let id = result + .get("id") + .ok_or_else(|| GolgiError::Sbot("id key not found on whoami call".to_string()))? + .as_str() + .ok_or_else(|| GolgiError::Sbot("whoami returned non-string value".to_string()))?; + Ok(id.to_string()) + } +} diff --git a/src/error.rs b/src/error.rs index 1c34248..010c834 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -//! Custom error type for `golgi`. +//! Custom error type. use std::io::Error as IoError; use std::str::Utf8Error; @@ -68,7 +68,7 @@ impl std::fmt::Display for GolgiError { // TODO: add context (what were we trying to decode?) GolgiError::DecodeBase64(_) => write!(f, "Failed to decode base64"), GolgiError::Io { ref context, .. } => write!(f, "IO error: {}", context), - GolgiError::Handshake(ref err) => write!(f, "{}", err), + GolgiError::Handshake(ref err) => write!(f, "Handshake failure: {}", err), GolgiError::Api(ref err) => write!(f, "SSB API failure: {}", err), GolgiError::Feed(ref err) => write!(f, "SSB feed error: {}", err), // TODO: improve this variant with a context message diff --git a/src/lib.rs b/src/lib.rs index ca26ae3..1869e24 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,31 +2,76 @@ //! # golgi //! -//! _The Golgi complex (aka. Golgi apparatus or Golgi body) packages proteins into membrane-bound vesicles inside the cell before the vesicles are sent to their destination._ +//! _The Golgi complex (aka. Golgi apparatus or Golgi body) packages proteins +//! into membrane-bound vesicles inside the cell before the vesicles are sent +//! to their destination._ //! //! ----- //! -//! Golgi is an experimental Scuttlebutt client which uses the [kuska-ssb](https://github.com/Kuska-ssb) libraries and aims to provide a high-level API for interacting with an sbot instance. Development efforts are currently oriented towards [go-sbot](https://github.com/cryptoscope/ssb) interoperability. +//! ## Introduction +//! +//! Golgi is an asynchronous, experimental Scuttlebutt client that aims to +//! facilitate Scuttlebutt application development. It provides a high-level +//! API for interacting with an sbot instance and uses the +//! [kuska-ssb](https://github.com/Kuska-ssb) libraries to make RPC calls. +//! Development efforts are currently oriented towards +//! [go-sbot](https://github.com/cryptoscope/ssb) interoperability. +//! +//! ## Features +//! +//! Golgi offers the ability to invoke individual RPC methods while also +//! providing a number of convenience methods which may involve multiple RPC +//! calls and / or the processing of data received from those calls. The +//! [`Sbot`](crate::sbot::Sbot) `struct` is the primary means of interacting +//! with the library. +//! +//! Features include the ability to publish messages of various kinds; to +//! retrieve messages (e.g. `about` and `description` messages) and formulate +//! queries; to follow, unfollow, block and unblock a peer; to query the social +//! graph; and to generate pub invite codes. +//! +//! Visit the [API modules](crate::api) to view the available methods. //! //! ## Example Usage //! +//! Basic usage is demonstrated below. Visit the [examples directory](https://git.coopcloud.tech/golgi-ssb/golgi/src/branch/main/examples) in the `golgi` repository for +//! more comprehensive examples. +//! //! ```rust -//! use golgi::GolgiError; -//! use golgi::sbot::Sbot; +//! use golgi::{messages::SsbMessageContent, GolgiError, Sbot}; //! //! pub async fn run() -> Result<(), GolgiError> { -//! let mut sbot_client = Sbot::connect(None, None).await?; +//! // Attempt to connect to an sbot instance using the default IP address, +//! // port and network key (aka. capabilities key). +//! let mut sbot_client = Sbot::init(None, None).await?; //! +//! // Call the `whoami` RPC method to retrieve the public key for the sbot +//! // identity. //! let id = sbot_client.whoami().await?; +//! +//! // Print the public key (identity) to `stdout`. //! println!("{}", id); //! +//! // Compose an SSB post message type. +//! let post = SsbMessageContent::Post { +//! text: "Biology, eh?!".to_string(), +//! mentions: None, +//! }; +//! +//! // Publish the post. +//! let post_msg_reference = sbot_client.publish(post).await?; +//! +//! // Print the reference (sigil-link) for the published post. +//! println!("{}", post_msg_reference); +//! //! Ok(()) //! } //! ``` +pub mod api; pub mod error; pub mod messages; pub mod sbot; pub mod utils; -pub use crate::error::GolgiError; +pub use crate::{error::GolgiError, sbot::Sbot}; diff --git a/src/messages.rs b/src/messages.rs index 35413f2..c26da6e 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -1,4 +1,4 @@ -//! Message types and conversion methods for `golgi`. +//! Message types and conversion methods. use kuska_ssb::api::dto::content::TypedMessage; use serde::{Deserialize, Serialize}; @@ -7,14 +7,17 @@ use std::fmt::Debug; use crate::error::GolgiError; -/// This is an alias to TypedMessage in kuska, -/// which is renamed as SsbMessageContent in golgi to fit the naming convention -/// of the other types (SsbMessageKVT and SsbMessageValue) +/// `SsbMessageContent` is a type alias for `TypedMessage` from the `kuska_ssb` library. +/// It is aliased in golgi to fit the naming convention of the other message +/// types: `SsbMessageKVT` and `SsbMessageValue`. +/// +/// See the [kuska source code](https://github.com/Kuska-ssb/ssb/blob/master/src/api/dto/content.rs#L103) for the type definition of `TypedMessage`. pub type SsbMessageContent = TypedMessage; -/// Data type representing the `value` of a message object (`KVT`). More information concerning the -/// data model can be found -/// in the [`Metadata` documentation](https://spec.scuttlebutt.nz/feed/messages.html#metadata). +/// The `value` of an SSB message (the `V` in `KVT`). +/// +/// More information concerning the data model can be found in the +/// [`Metadata` documentation](https://spec.scuttlebutt.nz/feed/messages.html#metadata). #[derive(Serialize, Deserialize, Debug)] #[serde(deny_unknown_fields)] #[allow(missing_docs)] @@ -28,7 +31,7 @@ pub struct SsbMessageValue { pub signature: String, } -// Enum representing the different possible message content types +/// Message content types. #[derive(Debug)] #[allow(missing_docs)] pub enum SsbMessageContentType { @@ -40,9 +43,13 @@ pub enum SsbMessageContentType { } impl SsbMessageValue { - /// Gets the type field of the message content as an enum, if found. - /// if no type field is found or the type field is not a string, it returns an Err(GolgiError::ContentType) - /// if a type field is found but with an unknown string it returns an Ok(SsbMessageContentType::Unrecognized) + /// Get the type field of the message content as an enum, if found. + /// + /// If no `type` field is found or the `type` field is not a string, + /// it returns an `Err(GolgiError::ContentType)`. + /// + /// If a `type` field is found but with an unknown string, + /// it returns an `Ok(SsbMessageContentType::Unrecognized)`. pub fn get_message_type(&self) -> Result { let msg_type = self .content @@ -61,8 +68,8 @@ impl SsbMessageValue { Ok(enum_type) } - /// Helper function which returns true if this message is of the given type, - /// and false if the type does not match or is not found + /// Helper function which returns `true` if this message is of the given type, + /// and `false` if the type does not match or is not found. pub fn is_message_type(&self, _message_type: SsbMessageContentType) -> bool { let self_message_type = self.get_message_type(); match self_message_type { @@ -73,20 +80,21 @@ impl SsbMessageValue { } } - /// Converts the content json value into an SsbMessageContent enum, - /// using the "type" field as a tag to select which variant of the enum + /// Convert the content JSON value into an `SsbMessageContent` `enum`, + /// using the `type` field as a tag to select which variant of the `enum` /// to deserialize into. /// - /// See more info on this here https://serde.rs/enum-representations.html#internally-tagged + /// See the [Serde docs on internally-tagged enum representations](https://serde.rs/enum-representations.html#internally-tagged) for further details. pub fn into_ssb_message_content(self) -> Result { let m: SsbMessageContent = serde_json::from_value(self.content)?; Ok(m) } } -/// Data type representing the `value` of a message object (`KVT`). More information concerning the -/// data model can be found -/// in the [`Metadata` documentation](https://spec.scuttlebutt.nz/feed/messages.html#metadata). +/// An SSB message represented as a key-value-timestamp (`KVT`). +/// +/// More information concerning the data model can be found in the +/// [`Metadata` documentation](https://spec.scuttlebutt.nz/feed/messages.html#metadata). #[derive(Serialize, Deserialize, Debug)] #[serde(deny_unknown_fields)] #[allow(missing_docs)] diff --git a/src/sbot/sbot_connection.rs b/src/sbot.rs similarity index 66% rename from src/sbot/sbot_connection.rs rename to src/sbot.rs index 0d06f63..40b958d 100644 --- a/src/sbot/sbot_connection.rs +++ b/src/sbot.rs @@ -1,4 +1,4 @@ -//! Sbot type and associated methods. +//! Sbot type and connection-related methods. use async_std::net::TcpStream; use kuska_handshake::async_std::BoxStream; @@ -10,20 +10,21 @@ use kuska_ssb::{ rpc::{RpcReader, RpcWriter}, }; -use crate::{error::GolgiError, utils}; +use crate::error::GolgiError; /// A struct representing a connection with a running sbot. /// A client and an rpc_reader can together be used to make requests to the sbot /// and read the responses. /// Note there can be multiple SbotConnection at the same time. pub struct SbotConnection { - /// client for writing requests to go-bot + /// Client for writing requests to go-bot pub client: ApiCaller, /// RpcReader object for reading responses from go-sbot pub rpc_reader: RpcReader, } -/// The Scuttlebutt identity, keys and configuration parameters for connecting to a local sbot +/// Holds the Scuttlebutt identity, keys and configuration parameters for +/// connecting to a local sbot and implements all Golgi API methods. pub struct Sbot { /// The ID (public key value) of the account associated with the local sbot instance. pub id: String, @@ -35,13 +36,10 @@ pub struct Sbot { } impl Sbot { - /// Initiate a connection with an sbot instance. Define the IP address, port and network key - /// for the sbot, then retrieve the public key, private key (secret) and identity from the - /// `.ssb-go/secret` file. Open a TCP stream to the sbot and perform the secret handshake. If successful, create a box stream and split it into a writer and reader. Return RPC handles to the sbot as part of the `struct` output. - pub async fn connect( - ip_port: Option, - net_id: Option, - ) -> Result { + /// Initiate a connection with an sbot instance. Define the IP address, + /// port and network key for the sbot, then retrieve the public key, + /// private key (secret) and identity from the `.ssb-go/secret` file. + pub async fn init(ip_port: Option, net_id: Option) -> Result { let address = if ip_port.is_none() { "127.0.0.1:8008".to_string() } else { @@ -67,8 +65,8 @@ impl Sbot { }) } - /// Creates a new connection with the sbot, - /// using the address, network_id, public_key and private_key supplied when Sbot was initialized. + /// Creates a new connection with the sbot, using the address, network_id, + /// public_key and private_key supplied when Sbot was initialized. /// /// Note that a single Sbot can have multiple SbotConnection at the same time. pub async fn get_sbot_connection(&self) -> Result { @@ -81,6 +79,10 @@ impl Sbot { /// Private helper function which creates a new connection with sbot, /// but with all variables passed as arguments. + /// + /// Open a TCP stream to the sbot and perform the secret handshake. If + /// successful, create a box stream and split it into a writer and reader. + /// Return RPC handles to the sbot as part of the `struct` output. async fn _get_sbot_connection_helper( address: String, network_id: auth::Key, @@ -91,7 +93,7 @@ impl Sbot { .await .map_err(|source| GolgiError::Io { source, - context: "socket error; failed to initiate tcp stream connection".to_string(), + context: "failed to initiate tcp stream connection".to_string(), })?; let handshake = kuska_handshake::async_std::handshake_client( @@ -112,24 +114,4 @@ impl Sbot { let sbot_connection = SbotConnection { rpc_reader, client }; Ok(sbot_connection) } - - /// Call the `whoami` RPC method and return an `id`. - pub async fn whoami(&mut self) -> Result { - let mut sbot_connection = self.get_sbot_connection().await?; - let req_id = sbot_connection.client.whoami_req_send().await?; - - let result = utils::get_async( - &mut sbot_connection.rpc_reader, - req_id, - utils::json_res_parse, - ) - .await?; - - let id = result - .get("id") - .ok_or_else(|| GolgiError::Sbot("id key not found on whoami call".to_string()))? - .as_str() - .ok_or_else(|| GolgiError::Sbot("whoami returned non-string value".to_string()))?; - Ok(id.to_string()) - } } diff --git a/src/sbot.rs_bak b/src/sbot.rs_bak deleted file mode 100644 index bca4a9e..0000000 --- a/src/sbot.rs_bak +++ /dev/null @@ -1,558 +0,0 @@ -//! Sbot type and associated methods. -use async_std::{ - net::TcpStream, - stream::{Stream, StreamExt}, -}; -use futures::pin_mut; -use std::collections::HashMap; - -use kuska_handshake::async_std::BoxStream; -use kuska_sodiumoxide::crypto::{auth, sign::ed25519}; -use kuska_ssb::{ - api::{dto::CreateHistoryStreamIn, ApiCaller}, - discovery, keystore, - keystore::OwnedIdentity, - rpc::{RpcReader, RpcWriter}, -}; - -use crate::error::GolgiError; -use crate::messages::{SsbMessageContent, SsbMessageContentType, SsbMessageKVT, SsbMessageValue}; -use crate::utils; -use crate::utils::get_source_stream; - -// re-export types from kuska -pub use kuska_ssb::api::dto::content::{ - FriendsHops, RelationshipQuery, SubsetQuery, SubsetQueryOptions, -}; - -/// A struct representing a connection with a running sbot. -/// A client and an rpc_reader can together be used to make requests to the sbot -/// and read the responses. -/// Note there can be multiple SbotConnection at the same time. -pub struct SbotConnection { - client: ApiCaller, - rpc_reader: RpcReader, -} - -/// The Scuttlebutt identity, keys and configuration parameters for connecting to a local sbot -pub struct Sbot { - pub id: String, - public_key: ed25519::PublicKey, - private_key: ed25519::SecretKey, - address: String, - // aka caps key (scuttleverse identifier) - network_id: auth::Key, -} - -impl Sbot { - /// Initiate a connection with an sbot instance. Define the IP address, port and network key - /// for the sbot, then retrieve the public key, private key (secret) and identity from the - /// `.ssb-go/secret` file. Open a TCP stream to the sbot and perform the secret handshake. If successful, create a box stream and split it into a writer and reader. Return RPC handles to the sbot as part of the `struct` output. - pub async fn init(ip_port: Option, net_id: Option) -> Result { - let address = if ip_port.is_none() { - "127.0.0.1:8008".to_string() - } else { - ip_port.unwrap() - }; - - let network_id = if net_id.is_none() { - discovery::ssb_net_id() - } else { - auth::Key::from_slice(&hex::decode(net_id.unwrap()).unwrap()).unwrap() - }; - - let OwnedIdentity { pk, sk, id } = keystore::from_gosbot_local() - .await - .expect("couldn't read local secret"); - - Ok(Self { - id, - public_key: pk, - private_key: sk, - address, - network_id, - }) - } - - /// Creates a new connection with the sbot, - /// using the address, network_id, public_key and private_key supplied when Sbot was initialized. - /// - /// Note that a single Sbot can have multiple SbotConnection at the same time. - pub async fn get_sbot_connection(&self) -> Result { - let address = self.address.clone(); - let network_id = self.network_id.clone(); - let public_key = self.public_key; - let private_key = self.private_key.clone(); - Sbot::_get_sbot_connection_helper(address, network_id, public_key, private_key).await - } - - /// Private helper function which creates a new connection with sbot, - /// but with all variables passed as arguments. - async fn _get_sbot_connection_helper( - address: String, - network_id: auth::Key, - public_key: ed25519::PublicKey, - private_key: ed25519::SecretKey, - ) -> Result { - let socket = TcpStream::connect(&address) - .await - .map_err(|source| GolgiError::Io { - source, - context: "socket error; failed to initiate tcp stream connection".to_string(), - })?; - - let handshake = kuska_handshake::async_std::handshake_client( - &mut &socket, - network_id.clone(), - public_key, - private_key.clone(), - public_key, - ) - .await - .map_err(GolgiError::Handshake)?; - - let (box_stream_read, box_stream_write) = - BoxStream::from_handshake(socket.clone(), socket, handshake, 0x8000).split_read_write(); - - let rpc_reader = RpcReader::new(box_stream_read); - let client = ApiCaller::new(RpcWriter::new(box_stream_write)); - let sbot_connection = SbotConnection { rpc_reader, client }; - Ok(sbot_connection) - } - - /// Call the `partialReplication getSubset` RPC method - /// and return a Stream of Result - /// - /// # Arguments - /// - /// * `query` - A `SubsetQuery` which specifies what filters to use. - /// * `option` - An Option<`SubsetQueryOptions`> which, if provided, adds additional - /// specifications to the query, such as specifying page limit and/or descending. - pub async fn get_subset_stream( - &mut self, - query: SubsetQuery, - options: Option, - ) -> Result>, GolgiError> { - let mut sbot_connection = self.get_sbot_connection().await?; - let req_id = sbot_connection - .client - .getsubset_req_send(query, options) - .await?; - let get_subset_stream = get_source_stream( - sbot_connection.rpc_reader, - req_id, - utils::ssb_message_res_parse, - ) - .await; - Ok(get_subset_stream) - } - - /// Call the `whoami` RPC method and return an `id`. - pub async fn whoami(&mut self) -> Result { - let mut sbot_connection = self.get_sbot_connection().await?; - let req_id = sbot_connection.client.whoami_req_send().await?; - - let result = utils::get_async( - &mut sbot_connection.rpc_reader, - req_id, - utils::json_res_parse, - ) - .await?; - - let id = result - .get("id") - .ok_or_else(|| GolgiError::Sbot("id key not found on whoami call".to_string()))? - .as_str() - .ok_or_else(|| GolgiError::Sbot("whoami returned non-string value".to_string()))?; - Ok(id.to_string()) - } - - /// Call the `publish` RPC method and return a message reference. - /// - /// # Arguments - /// - /// * `msg` - A `SsbMessageContent` `enum` whose variants include `Pub`, `Post`, `Contact`, `About`, - /// `Channel` and `Vote`. See the `kuska_ssb` documentation for further details such as field - /// names and accepted values for each variant. - pub async fn publish(&mut self, msg: SsbMessageContent) -> Result { - let mut sbot_connection = self.get_sbot_connection().await?; - let req_id = sbot_connection.client.publish_req_send(msg).await?; - - utils::get_async( - &mut sbot_connection.rpc_reader, - req_id, - utils::string_res_parse, - ) - .await - } - - // Convenience method to set a relationship with following: true, blocking: false - pub async fn follow(&mut self, contact: &str) -> Result { - self.set_relationship(contact, true, false).await - } - - // Convenience method to set a relationship with following: false, blocking: true - pub async fn block(&mut self, contact: &str) -> Result { - self.set_relationship(contact, false, true).await - } - - /// Publishes a contact relationship to the given user (with ssb_id) with the given state. - pub async fn set_relationship( - &mut self, - contact: &str, - following: bool, - blocking: bool, - ) -> Result { - let msg = SsbMessageContent::Contact { - contact: Some(contact.to_string()), - following: Some(following), - blocking: Some(blocking), - autofollow: None, - }; - self.publish(msg).await - } - - /* - /// Call the `friends isFollowing` RPC method and return a message reference. - /// Returns true if src_id is following dest_id and false otherwise. - pub async fn friends_is_following( - &mut self, - args: RelationshipQuery, - ) -> Result { - let mut sbot_connection = self.get_sbot_connection().await?; - let req_id = sbot_connection - .client - .friends_is_following_req_send(args) - .await?; - - utils::get_async( - &mut sbot_connection.rpc_reader, - req_id, - utils::string_res_parse, - ) - .await - } - - /// Call the `friends isblocking` RPC method and return a message reference. - /// Returns true if src_id is blocking dest_id and false otherwise. - pub async fn friends_is_blocking( - &mut self, - args: RelationshipQuery, - ) -> Result { - let mut sbot_connection = self.get_sbot_connection().await?; - let req_id = sbot_connection - .client - .friends_is_blocking_req_send(args) - .await?; - - utils::get_async( - &mut sbot_connection.rpc_reader, - req_id, - utils::string_res_parse, - ) - .await - } - - // Gets a Vec where each element is a peer you are following - pub async fn get_follows(&mut self) -> Result, GolgiError> { - self.friends_hops(FriendsHops { - max: 1, - start: None, - reverse: Some(false), - }) - .await - } - - // Gets a Vec where each element is a peer who follows you - /// TODO: currently this method is not working - /// go-sbot does not seem to listen to the reverse=True parameter - /// and just returns follows - async fn get_followers(&mut self) -> Result, GolgiError> { - self.friends_hops(FriendsHops { - max: 1, - start: None, - reverse: Some(true), - }) - .await - } - - /// Call the `friends hops` RPC method and return a Vector - /// where each element of the vector is the ssb_id of a peer. - /// - /// When opts.reverse = True, it should return peers who are following you - /// (but this is currently not working) - pub async fn friends_hops(&mut self, args: FriendsHops) -> Result, GolgiError> { - let mut sbot_connection = self.get_sbot_connection().await?; - let req_id = sbot_connection.client.friends_hops_req_send(args).await?; - utils::get_source_until_eof( - &mut sbot_connection.rpc_reader, - req_id, - utils::string_res_parse, - ) - .await - } - */ - - /// Call the `invite create` RPC method and return the created invite - pub async fn invite_create(&mut self, uses: u16) -> Result { - let mut sbot_connection = self.get_sbot_connection().await?; - let req_id = sbot_connection.client.invite_create_req_send(uses).await?; - - utils::get_async( - &mut sbot_connection.rpc_reader, - req_id, - utils::string_res_parse, - ) - .await - } - - /// Call the `invite use` RPC method and return a reference to the message. - pub async fn invite_use(&mut self, invite_code: &str) -> Result { - let mut sbot_connection = self.get_sbot_connection().await?; - let req_id = sbot_connection - .client - .invite_use_req_send(invite_code) - .await?; - - utils::get_async( - &mut sbot_connection.rpc_reader, - req_id, - utils::string_res_parse, - ) - .await - } - - /// Wrapper for publish which constructs and publishes a post message appropriately from a string. - /// - /// # Arguments - /// - /// * `text` - A reference to a string slice which represents the text to be published in the post - pub async fn publish_post(&mut self, text: &str) -> Result { - let msg = SsbMessageContent::Post { - text: text.to_string(), - mentions: None, - }; - self.publish(msg).await - } - - /// Wrapper for publish which constructs and publishes an about description message appropriately from a string. - /// - /// # Arguments - /// - /// * `description` - A reference to a string slice which represents the text to be published as an about description. - pub async fn publish_description(&mut self, description: &str) -> Result { - let msg = SsbMessageContent::About { - about: self.id.to_string(), - name: None, - title: None, - branch: None, - image: None, - description: Some(description.to_string()), - location: None, - start_datetime: None, - }; - self.publish(msg).await - } - - /// Wrapper for publish which constructs and publishes an about name message appropriately from a string. - /// - /// # Arguments - /// - /// * `name` - A reference to a string slice which represents the text to be published as an about name. - pub async fn publish_name(&mut self, name: &str) -> Result { - let msg = SsbMessageContent::About { - about: self.id.to_string(), - name: Some(name.to_string()), - title: None, - branch: None, - image: None, - description: None, - location: None, - start_datetime: None, - }; - self.publish(msg).await - } - - /// Get the about messages for a particular user in order of recency. - pub async fn get_about_message_stream( - &mut self, - ssb_id: &str, - ) -> Result>, GolgiError> { - let query = SubsetQuery::Author { - op: "author".to_string(), - feed: ssb_id.to_string(), - }; - // specify that most recent messages should be returned first - let query_options = SubsetQueryOptions { - descending: Some(true), - keys: None, - page_limit: None, - }; - let get_subset_stream = self.get_subset_stream(query, Some(query_options)).await?; - // TODO: after fixing sbot regression, - // change this subset query to filter by type about in addition to author - // and remove this filter section - // filter down to about messages - let about_message_stream = get_subset_stream.filter(|msg| match msg { - Ok(val) => val.is_message_type(SsbMessageContentType::About), - Err(_err) => false, - }); - // return about message stream - Ok(about_message_stream) - } - - /// Get value of latest about message with given key from given user - pub async fn get_latest_about_message( - &mut self, - ssb_id: &str, - key: &str, - ) -> Result, GolgiError> { - // get about_message_stream - let about_message_stream = self.get_about_message_stream(ssb_id).await?; - // now we have a stream of about messages with most recent at the front of the vector - pin_mut!(about_message_stream); - // iterate through the vector looking for most recent about message with the given key - let latest_about_message_res: Option> = - about_message_stream - // find the first msg that contains the field `key` - .find(|res| match res { - Ok(msg) => msg.content.get(key).is_some(), - Err(_) => false, - }) - .await; - // Option> -> Option - let latest_about_message = latest_about_message_res.and_then(|msg| msg.ok()); - // Option -> Option - let latest_about_value = latest_about_message.and_then(|msg| { - msg - // SsbMessageValue -> Option<&Value> - .content - .get(key) - // Option<&Value> -> - .and_then(|value| value.as_str()) - // Option<&str> -> Option - .map(|value| value.to_string()) - }); - // return value is either `Ok(Some(String))` or `Ok(None)` - Ok(latest_about_value) - } - - /// Get HashMap of profile info for given user - pub async fn get_profile_info( - &mut self, - ssb_id: &str, - ) -> Result, GolgiError> { - let mut keys_to_search_for = vec!["name", "description", "image"]; - self.get_about_info(ssb_id, keys_to_search_for).await - } - - /// Get HashMap of name and image for given user - /// (this is can be used to display profile images of a list of users) - pub async fn get_name_and_image( - &mut self, - ssb_id: &str, - ) -> Result, GolgiError> { - let mut keys_to_search_for = vec!["name", "image"]; - self.get_about_info(ssb_id, keys_to_search_for).await - } - - /// Get HashMap of about keys to values for given user - /// by iteratively searching through a stream of about messages, - /// in order of recency, - /// until we find all about messages for all needed info - /// or reach the end of the stream. - /// - /// # Arguments - /// - /// * `ssb_id` - A reference to a string slice which represents the id of the user to get info about. - /// * `keys_to_search_for` - A mutable vector of string slice, which represent the about keys - /// that will be searched for. As they are found, keys are removed from the vector. - pub async fn get_about_info( - &mut self, - ssb_id: &str, - mut keys_to_search_for: Vec<&str>, - ) -> Result, GolgiError> { - // get about_message_stream - let about_message_stream = self.get_about_message_stream(ssb_id).await?; - // now we have a stream of about messages with most recent at the front of the vector - pin_mut!(about_message_stream); // needed for iteration - let mut profile_info: HashMap = HashMap::new(); - // iterate through the stream while it still has more values and - // we still have keys we are looking for - while let Some(res) = about_message_stream.next().await { - // if there are no more keys we are looking for, then we are done - if keys_to_search_for.len() == 0 { - break; - } - // if there are still keys we are looking for, then continue searching - match res { - Ok(msg) => { - // for each key we are searching for, check if this about - // message contains a value for that key - for key in &keys_to_search_for.clone() { - let option_val = msg - .content - .get(key) - .and_then(|val| val.as_str()) - .map(|val| val.to_string()); - match option_val { - Some(val) => { - // if a value is found, then insert it - profile_info.insert(key.to_string(), val); - // remove this key fom keys_to_search_for, since we are no longer searching for it - keys_to_search_for.retain(|val| val != key) - } - None => continue, - } - } - } - Err(err) => { - // skip errors - continue; - } - } - } - Ok(profile_info) - } - - /// Get latest about name from given user - /// - /// # Arguments - /// - /// * `ssb_id` - A reference to a string slice which represents the ssb user - /// to lookup the about name for. - pub async fn get_name(&mut self, ssb_id: &str) -> Result, GolgiError> { - self.get_latest_about_message(ssb_id, "name").await - } - - /// Get latest about description from given user - /// - /// # Arguments - /// - /// * `ssb_id` - A reference to a string slice which represents the ssb user - /// to lookup the about description for. - pub async fn get_description(&mut self, ssb_id: &str) -> Result, GolgiError> { - self.get_latest_about_message(ssb_id, "description").await - } - - /// Call the `createHistoryStream` RPC method - /// and return a Stream of Result - pub async fn create_history_stream( - &mut self, - id: String, - ) -> Result>, GolgiError> { - let mut sbot_connection = self.get_sbot_connection().await?; - let args = CreateHistoryStreamIn::new(id); - let req_id = sbot_connection - .client - .create_history_stream_req_send(&args) - .await?; - let history_stream = get_source_stream( - sbot_connection.rpc_reader, - req_id, - utils::ssb_message_res_parse, - ) - .await; - Ok(history_stream) - } -} diff --git a/src/sbot/about.rs b/src/sbot/about.rs deleted file mode 100644 index 3a8837a..0000000 --- a/src/sbot/about.rs +++ /dev/null @@ -1,177 +0,0 @@ -use std::collections::HashMap; - -use async_std::stream::{Stream, StreamExt}; -use futures::pin_mut; - -use crate::{ - error::GolgiError, - messages::{SsbMessageContentType, SsbMessageValue}, - sbot::{ - get_subset::{SubsetQuery, SubsetQueryOptions}, - sbot_connection::Sbot, - }, -}; - -impl Sbot { - /// Get the about messages for a particular user in order of recency. - pub async fn get_about_message_stream( - &mut self, - ssb_id: &str, - ) -> Result>, GolgiError> { - let query = SubsetQuery::Author { - op: "author".to_string(), - feed: ssb_id.to_string(), - }; - // specify that most recent messages should be returned first - let query_options = SubsetQueryOptions { - descending: Some(true), - keys: None, - page_limit: None, - }; - let get_subset_stream = self.get_subset_stream(query, Some(query_options)).await?; - // TODO: after fixing sbot regression, - // change this subset query to filter by type about in addition to author - // and remove this filter section - // filter down to about messages - let about_message_stream = get_subset_stream.filter(|msg| match msg { - Ok(val) => val.is_message_type(SsbMessageContentType::About), - Err(_err) => false, - }); - // return about message stream - Ok(about_message_stream) - } - - /// Get value of latest about message with given key from given user - pub async fn get_latest_about_message( - &mut self, - ssb_id: &str, - key: &str, - ) -> Result, GolgiError> { - // get about_message_stream - let about_message_stream = self.get_about_message_stream(ssb_id).await?; - // now we have a stream of about messages with most recent at the front of the vector - pin_mut!(about_message_stream); - // iterate through the vector looking for most recent about message with the given key - let latest_about_message_res: Option> = - about_message_stream - // find the first msg that contains the field `key` - .find(|res| match res { - Ok(msg) => msg.content.get(key).is_some(), - Err(_) => false, - }) - .await; - // Option> -> Option - let latest_about_message = latest_about_message_res.and_then(|msg| msg.ok()); - // Option -> Option - let latest_about_value = latest_about_message.and_then(|msg| { - msg - // SsbMessageValue -> Option<&Value> - .content - .get(key) - // Option<&Value> -> - .and_then(|value| value.as_str()) - // Option<&str> -> Option - .map(|value| value.to_string()) - }); - // return value is either `Ok(Some(String))` or `Ok(None)` - Ok(latest_about_value) - } - - /// Get HashMap of profile info for given user - pub async fn get_profile_info( - &mut self, - ssb_id: &str, - ) -> Result, GolgiError> { - let keys_to_search_for = vec!["name", "description", "image"]; - self.get_about_info(ssb_id, keys_to_search_for).await - } - - /// Get HashMap of name and image for given user - /// (this is can be used to display profile images of a list of users) - pub async fn get_name_and_image( - &mut self, - ssb_id: &str, - ) -> Result, GolgiError> { - let keys_to_search_for = vec!["name", "image"]; - self.get_about_info(ssb_id, keys_to_search_for).await - } - - /// Get HashMap of about keys to values for given user - /// by iteratively searching through a stream of about messages, - /// in order of recency, - /// until we find all about messages for all needed info - /// or reach the end of the stream. - /// - /// # Arguments - /// - /// * `ssb_id` - A reference to a string slice which represents the id of the user to get info about. - /// * `keys_to_search_for` - A mutable vector of string slice, which represent the about keys - /// that will be searched for. As they are found, keys are removed from the vector. - pub async fn get_about_info( - &mut self, - ssb_id: &str, - mut keys_to_search_for: Vec<&str>, - ) -> Result, GolgiError> { - // get about_message_stream - let about_message_stream = self.get_about_message_stream(ssb_id).await?; - // now we have a stream of about messages with most recent at the front of the vector - pin_mut!(about_message_stream); // needed for iteration - let mut profile_info: HashMap = HashMap::new(); - // iterate through the stream while it still has more values and - // we still have keys we are looking for - while let Some(res) = about_message_stream.next().await { - // if there are no more keys we are looking for, then we are done - if keys_to_search_for.is_empty() { - break; - } - // if there are still keys we are looking for, then continue searching - match res { - Ok(msg) => { - // for each key we are searching for, check if this about - // message contains a value for that key - for key in &keys_to_search_for.clone() { - let option_val = msg - .content - .get(key) - .and_then(|val| val.as_str()) - .map(|val| val.to_string()); - match option_val { - Some(val) => { - // if a value is found, then insert it - profile_info.insert(key.to_string(), val); - // remove this key fom keys_to_search_for, since we are no longer searching for it - keys_to_search_for.retain(|val| val != key) - } - None => continue, - } - } - } - Err(_err) => { - // skip errors - continue; - } - } - } - Ok(profile_info) - } - - /// Get latest about name from given user - /// - /// # Arguments - /// - /// * `ssb_id` - A reference to a string slice which represents the ssb user - /// to lookup the about name for. - pub async fn get_name(&mut self, ssb_id: &str) -> Result, GolgiError> { - self.get_latest_about_message(ssb_id, "name").await - } - - /// Get latest about description from given user - /// - /// # Arguments - /// - /// * `ssb_id` - A reference to a string slice which represents the ssb user - /// to lookup the about description for. - pub async fn get_description(&mut self, ssb_id: &str) -> Result, GolgiError> { - self.get_latest_about_message(ssb_id, "description").await - } -} diff --git a/src/sbot/friends.rs b/src/sbot/friends.rs deleted file mode 100644 index f663eb0..0000000 --- a/src/sbot/friends.rs +++ /dev/null @@ -1,110 +0,0 @@ -use kuska_ssb::api::dto::content::{FriendsHops, RelationshipQuery}; - -use crate::{error::GolgiError, messages::SsbMessageContent, sbot::sbot_connection::Sbot, utils}; - -impl Sbot { - /// Convenience method to set a relationship with following: true, blocking: false. - pub async fn follow(&mut self, contact: &str) -> Result { - self.set_relationship(contact, true, false).await - } - - /// Convenience method to set a relationship with following: false, blocking: true. - pub async fn block(&mut self, contact: &str) -> Result { - self.set_relationship(contact, false, true).await - } - - /// Publishes a contact relationship to the given user (with ssb_id) with the given state. - pub async fn set_relationship( - &mut self, - contact: &str, - following: bool, - blocking: bool, - ) -> Result { - let msg = SsbMessageContent::Contact { - contact: Some(contact.to_string()), - following: Some(following), - blocking: Some(blocking), - autofollow: None, - }; - self.publish(msg).await - } - - /// Call the `friends isFollowing` RPC method and return a message reference. - /// Returns true if src_id is following dest_id and false otherwise. - pub async fn friends_is_following( - &mut self, - args: RelationshipQuery, - ) -> Result { - let mut sbot_connection = self.get_sbot_connection().await?; - let req_id = sbot_connection - .client - .friends_is_following_req_send(args) - .await?; - - utils::get_async( - &mut sbot_connection.rpc_reader, - req_id, - utils::string_res_parse, - ) - .await - } - - /// Call the `friends isblocking` RPC method and return a message reference. - /// Returns true if src_id is blocking dest_id and false otherwise. - pub async fn friends_is_blocking( - &mut self, - args: RelationshipQuery, - ) -> Result { - let mut sbot_connection = self.get_sbot_connection().await?; - let req_id = sbot_connection - .client - .friends_is_blocking_req_send(args) - .await?; - - utils::get_async( - &mut sbot_connection.rpc_reader, - req_id, - utils::string_res_parse, - ) - .await - } - - /// Return a Vec where each element is a peer you are following. - pub async fn get_follows(&mut self) -> Result, GolgiError> { - self.friends_hops(FriendsHops { - max: 1, - start: None, - reverse: Some(false), - }) - .await - } - - // Gets a Vec where each element is a peer who follows you - /// TODO: currently this method is not working - /// go-sbot does not seem to listen to the reverse=True parameter - /// and just returns follows - async fn _get_followers(&mut self) -> Result, GolgiError> { - self.friends_hops(FriendsHops { - max: 1, - start: None, - reverse: Some(true), - }) - .await - } - - /// Call the `friends hops` RPC method and return a Vector - /// where each element of the vector is the ssb_id of a peer. - /// - /// When opts.reverse = True, it should return peers who are following you - /// (but this is currently not working) - pub async fn friends_hops(&mut self, args: FriendsHops) -> Result, GolgiError> { - let mut sbot_connection = self.get_sbot_connection().await?; - let req_id = sbot_connection.client.friends_hops_req_send(args).await?; - utils::get_source_until_eof( - &mut sbot_connection.rpc_reader, - req_id, - utils::string_res_parse, - ) - .await - } -} diff --git a/src/sbot/get_subset.rs b/src/sbot/get_subset.rs deleted file mode 100644 index abdc3d0..0000000 --- a/src/sbot/get_subset.rs +++ /dev/null @@ -1,37 +0,0 @@ -use async_std::stream::Stream; - -use crate::{ - error::GolgiError, messages::SsbMessageValue, sbot::sbot_connection::Sbot, utils, - utils::get_source_stream, -}; - -pub use kuska_ssb::api::dto::content::{SubsetQuery, SubsetQueryOptions}; - -impl Sbot { - /// Call the `partialReplication getSubset` RPC method - /// and return a Stream of Result - /// - /// # Arguments - /// - /// * `query` - A `SubsetQuery` which specifies what filters to use. - /// * `option` - An Option<`SubsetQueryOptions`> which, if provided, adds additional - /// specifications to the query, such as specifying page limit and/or descending. - pub async fn get_subset_stream( - &mut self, - query: SubsetQuery, - options: Option, - ) -> Result>, GolgiError> { - let mut sbot_connection = self.get_sbot_connection().await?; - let req_id = sbot_connection - .client - .getsubset_req_send(query, options) - .await?; - let get_subset_stream = get_source_stream( - sbot_connection.rpc_reader, - req_id, - utils::ssb_message_res_parse, - ) - .await; - Ok(get_subset_stream) - } -} diff --git a/src/sbot/history_stream.rs b/src/sbot/history_stream.rs deleted file mode 100644 index 3497322..0000000 --- a/src/sbot/history_stream.rs +++ /dev/null @@ -1,30 +0,0 @@ -use async_std::stream::Stream; -use kuska_ssb::api::dto::CreateHistoryStreamIn; - -use crate::{ - error::GolgiError, messages::SsbMessageValue, sbot::sbot_connection::Sbot, utils, - utils::get_source_stream, -}; - -impl Sbot { - /// Call the `createHistoryStream` RPC method - /// and return a Stream of Result. - pub async fn create_history_stream( - &mut self, - id: String, - ) -> Result>, GolgiError> { - let mut sbot_connection = self.get_sbot_connection().await?; - let args = CreateHistoryStreamIn::new(id); - let req_id = sbot_connection - .client - .create_history_stream_req_send(&args) - .await?; - let history_stream = get_source_stream( - sbot_connection.rpc_reader, - req_id, - utils::ssb_message_res_parse, - ) - .await; - Ok(history_stream) - } -} diff --git a/src/sbot/invite.rs b/src/sbot/invite.rs deleted file mode 100644 index 8b04437..0000000 --- a/src/sbot/invite.rs +++ /dev/null @@ -1,32 +0,0 @@ -use crate::{error::GolgiError, sbot::sbot_connection::Sbot, utils}; - -impl Sbot { - /// Call the `invite create` RPC method and return the created invite - pub async fn invite_create(&mut self, uses: u16) -> Result { - let mut sbot_connection = self.get_sbot_connection().await?; - let req_id = sbot_connection.client.invite_create_req_send(uses).await?; - - utils::get_async( - &mut sbot_connection.rpc_reader, - req_id, - utils::string_res_parse, - ) - .await - } - - /// Call the `invite use` RPC method and return a reference to the message. - pub async fn invite_use(&mut self, invite_code: &str) -> Result { - let mut sbot_connection = self.get_sbot_connection().await?; - let req_id = sbot_connection - .client - .invite_use_req_send(invite_code) - .await?; - - utils::get_async( - &mut sbot_connection.rpc_reader, - req_id, - utils::string_res_parse, - ) - .await - } -} diff --git a/src/sbot/mod.rs b/src/sbot/mod.rs deleted file mode 100644 index a20918a..0000000 --- a/src/sbot/mod.rs +++ /dev/null @@ -1,15 +0,0 @@ -//! This module contains the golgi API for interacting with a running go-sbot instance. -mod about; -mod friends; -mod get_subset; -mod history_stream; -mod invite; -mod publish; -mod sbot_connection; - -pub use sbot_connection::*; - -// re-export types from kuska -pub use kuska_ssb::api::dto::content::{ - FriendsHops, RelationshipQuery, SubsetQuery, SubsetQueryOptions, -}; diff --git a/src/sbot/publish.rs b/src/sbot/publish.rs deleted file mode 100644 index d6a8174..0000000 --- a/src/sbot/publish.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::{error::GolgiError, messages::SsbMessageContent, sbot::sbot_connection::Sbot, utils}; - -impl Sbot { - /// Call the `publish` RPC method and return a message reference. - /// - /// # Arguments - /// - /// * `msg` - A `SsbMessageContent` `enum` whose variants include `Pub`, `Post`, `Contact`, `About`, - /// `Channel` and `Vote`. See the `kuska_ssb` documentation for further details such as field - /// names and accepted values for each variant. - pub async fn publish(&mut self, msg: SsbMessageContent) -> Result { - let mut sbot_connection = self.get_sbot_connection().await?; - let req_id = sbot_connection.client.publish_req_send(msg).await?; - - utils::get_async( - &mut sbot_connection.rpc_reader, - req_id, - utils::string_res_parse, - ) - .await - } - - /// Wrapper for publish which constructs and publishes a post message appropriately from a string. - /// - /// # Arguments - /// - /// * `text` - A reference to a string slice which represents the text to be published in the post - pub async fn publish_post(&mut self, text: &str) -> Result { - let msg = SsbMessageContent::Post { - text: text.to_string(), - mentions: None, - }; - self.publish(msg).await - } - - /// Wrapper for publish which constructs and publishes an about description message appropriately from a string. - /// - /// # Arguments - /// - /// * `description` - A reference to a string slice which represents the text to be published as an about description. - pub async fn publish_description(&mut self, description: &str) -> Result { - let msg = SsbMessageContent::About { - about: self.id.to_string(), - name: None, - title: None, - branch: None, - image: None, - description: Some(description.to_string()), - location: None, - start_datetime: None, - }; - self.publish(msg).await - } - - /// Wrapper for publish which constructs and publishes an about name message appropriately from a string. - /// - /// # Arguments - /// - /// * `name` - A reference to a string slice which represents the text to be published as an about name. - pub async fn publish_name(&mut self, name: &str) -> Result { - let msg = SsbMessageContent::About { - about: self.id.to_string(), - name: Some(name.to_string()), - title: None, - branch: None, - image: None, - description: None, - location: None, - start_datetime: None, - }; - self.publish(msg).await - } -} diff --git a/src/utils.rs b/src/utils.rs index 97f8323..5871549 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,4 +1,4 @@ -//! Utility methods for `golgi`. +//! Utility methods. use std::fmt::Debug; use async_std::{io::Read, net::TcpStream, stream::Stream}; @@ -9,7 +9,7 @@ use serde_json::Value; use crate::error::GolgiError; use crate::messages::{SsbMessageKVT, SsbMessageValue}; -/// Function to parse an array of bytes (returned by an rpc call) into a KVT. +/// Parse an array of bytes (returned by an rpc call) into a `KVT`. /// /// # Arguments /// @@ -20,7 +20,7 @@ pub fn kvt_res_parse(body: &[u8]) -> Result { Ok(kvt) } -/// Function to parse an array of bytes (returned by an rpc call) into a String. +/// Parse an array of bytes (returned by an rpc call) into a `String`. /// /// # Arguments /// @@ -29,7 +29,7 @@ pub fn string_res_parse(body: &[u8]) -> Result { Ok(std::str::from_utf8(body)?.to_string()) } -/// Function to parse an array of bytes (returned by an rpc call) into a serde_json::Value. +/// Parse an array of bytes (returned by an rpc call) into a `serde_json::Value`. /// /// # Arguments /// @@ -39,7 +39,7 @@ pub fn json_res_parse(body: &[u8]) -> Result { Ok(message) } -/// Function to parse an array of bytes (returned by an rpc call) into an SsbMessageValue +/// Parse an array of bytes (returned by an rpc call) into an `SsbMessageValue`. /// /// # Arguments /// @@ -49,17 +49,18 @@ pub fn ssb_message_res_parse(body: &[u8]) -> Result Ok(message) } -/// Takes in an rpc request number, and a handling function, -/// and waits for an rpc response which matches the request number, -/// and then calls the handling function on the response. +/// Take in an RPC request number along with a handling function and wait for +/// an RPC response which matches the request number. Then, call the handling +/// function on the response. /// /// # Arguments /// /// * `rpc_reader` - A `RpcReader` which can return Messages in a loop /// * `req_no` - A `RequestNo` of the response to listen for -/// * `f` - A function which takes in an array of u8 and returns a Result. -/// This is a function which parses the response from the RpcReader. T is a generic type, -/// so this parse function can return multiple possible types (String, json, custom struct etc.) +/// * `f` - A function which takes in an array of `u8` and returns a +/// `Result`. This is a function which parses the response from +/// the `RpcReader`. `T` is a generic type, so this parse function can return +/// multiple possible types (`String`, JSON, custom struct etc.) pub async fn get_async<'a, R, T, F>( rpc_reader: &mut RpcReader, req_no: RequestNo, @@ -91,19 +92,19 @@ where } } -/// Takes in an rpc request number, and a handling function, -/// and calls the handling function on all RPC responses which match the request number, -/// appending the result of each parsed message to a vector, -/// until a CancelStreamResponse is found, marking the end of the stream, -/// and then finally a result is returned. +/// Take in an RPC request number along with a handling function and call +/// the handling function on all RPC responses which match the request number, +/// appending the result of each parsed message to a vector until a +/// `CancelStreamResponse` is found (marking the end of the stream). /// /// # Arguments /// /// * `rpc_reader` - A `RpcReader` which can return Messages in a loop /// * `req_no` - A `RequestNo` of the response to listen for -/// * `f` - A function which takes in an array of u8 and returns a Result. -/// This is a function which parses the response from the RpcReader. T is a generic type, -/// so this parse function can return multiple possible types (String, json, custom struct etc.) +/// * `f` - A function which takes in an array of `u8` and returns a +/// `Result`. This is a function which parses the response from +/// the `RpcReader`. `T` is a generic type, so this parse function can return +/// multiple possible types (`String`, JSON, custom struct etc.) pub async fn get_source_until_eof<'a, R, T, F>( rpc_reader: &mut RpcReader, req_no: RequestNo, @@ -141,19 +142,20 @@ where Ok(messages) } -/// Takes in an rpc request number, and a handling function, -/// and calls the handling function on all responses which match the request number, -/// and prints out the result of the handling function. +/// Take in an RPC request number along with a handling function and call the +/// handling function on all responses which match the request number. Then, +/// prints out the result of the handling function. /// -/// This is a function useful for debugging, and only prints the output. +/// This function useful for debugging and only prints the output. /// /// # Arguments /// /// * `rpc_reader` - A `RpcReader` which can return Messages in a loop /// * `req_no` - A `RequestNo` of the response to listen for -/// * `f` - A function which takes in an array of u8 and returns a Result. -/// This is a function which parses the response from the RpcReader. T is a generic type, -/// so this parse function can return multiple possible types (String, json, custom struct etc.) +/// * `f` - A function which takes in an array of `u8` and returns a +/// `Result`. This is a function which parses the response from +/// the `RpcReader`. `T` is a generic type, so this parse function can return +/// multiple possible types (`String`, JSON, custom struct etc.) pub async fn print_source_until_eof<'a, R, T, F>( rpc_reader: &mut RpcReader, req_no: RequestNo, @@ -183,17 +185,18 @@ where Ok(()) } -/// Takes in an rpc request number, and a handling function (parsing results of type T), -/// and produces an async_std::stream::Stream -/// of results of type T where the handling function is called -/// on all rpc_reader responses which match the request number. +/// Take in an RPC request number along with a handling function (parsing +/// results of type `T`) and produce an `async_std::stream::Stream` of results +/// of type `T`, where the handling function is called on all `RpcReader` +/// responses which match the request number. /// /// # Arguments /// /// * `req_no` - A `RequestNo` of the response to listen for -/// * `f` - A function which takes in an array of u8 and returns a Result. -/// This is a function which parses the response from the RpcReader. T is a generic type, -/// so this parse function can return multiple possible types (String, json, custom struct etc.) +/// * `f` - A function which takes in an array of `u8` and returns a +/// `Result`. This is a function which parses the response from +/// the `RpcReader`. `T` is a generic type, so this parse function can return +/// multiple possible types (`String`, JSON, custom struct etc.) pub async fn get_source_stream<'a, F, T>( mut rpc_reader: RpcReader, req_no: RequestNo,