diff --git a/examples/ssb-client.rs b/examples/ssb-client.rs index 10fea55..9e0b4ad 100644 --- a/examples/ssb-client.rs +++ b/examples/ssb-client.rs @@ -42,6 +42,14 @@ async fn run() -> Result<(), GolgiError> { 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(()) } diff --git a/examples/ssb-friends.rs b/examples/ssb-friends.rs new file mode 100644 index 0000000..1ce339f --- /dev/null +++ b/examples/ssb-friends.rs @@ -0,0 +1,86 @@ +use async_std::stream::StreamExt; +use futures::pin_mut; +use std::process; + +use golgi::error::GolgiError; +use golgi::messages::SsbMessageContent; +use golgi::sbot::Sbot; +use golgi::sbot::{FriendsHops, RelationshipQuery, SubsetQuery, SubsetQueryOptions}; + +async fn run() -> Result<(), GolgiError> { + let mut sbot_client = Sbot::init(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/git_hooks/pre-commit b/git_hooks/pre-commit new file mode 100755 index 0000000..f3fb918 --- /dev/null +++ b/git_hooks/pre-commit @@ -0,0 +1 @@ +cargo fmt diff --git a/src/sbot.rs b/src/sbot.rs index decc427..9f95744 100644 --- a/src/sbot.rs +++ b/src/sbot.rs @@ -1,10 +1,10 @@ //! Sbot type and associated methods. -use std::collections::HashMap; 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}; @@ -21,7 +21,9 @@ use crate::utils; use crate::utils::get_source_stream; // re-export types from kuska -pub use kuska_ssb::api::dto::content::{SubsetQuery, SubsetQueryOptions}; +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 @@ -33,7 +35,6 @@ pub struct SbotConnection { } /// The Scuttlebutt identity, keys and configuration parameters for connecting to a local sbot -/// instance, as well as handles for calling RPC methods and receiving responses. pub struct Sbot { pub id: String, public_key: ed25519::PublicKey, @@ -41,9 +42,6 @@ pub struct Sbot { address: String, // aka caps key (scuttleverse identifier) network_id: auth::Key, - // the primary connection with sbot which can be re-used for non-stream calls - // note that stream calls will each need their own SbotConnection - sbot_connection: SbotConnection, } impl Sbot { @@ -67,17 +65,12 @@ impl Sbot { .await .expect("couldn't read local secret"); - let sbot_connection = - Sbot::_get_sbot_connection_helper(address.clone(), network_id.clone(), pk, sk.clone()) - .await?; - Ok(Self { id, public_key: pk, private_key: sk, address, network_id, - sbot_connection, }) } @@ -145,17 +138,22 @@ impl Sbot { .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; + 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 req_id = self.sbot_connection.client.whoami_req_send().await?; + 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 self.sbot_connection.rpc_reader, + &mut sbot_connection.rpc_reader, req_id, utils::json_res_parse, ) @@ -177,10 +175,116 @@ impl Sbot { /// `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 req_id = self.sbot_connection.client.publish_req_send(msg).await?; + let mut sbot_connection = self.get_sbot_connection().await?; + let req_id = sbot_connection.client.publish_req_send(msg).await?; utils::get_async( - &mut self.sbot_connection.rpc_reader, + &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, ) @@ -259,9 +363,7 @@ impl Sbot { // 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) - } + Ok(val) => val.is_message_type(SsbMessageContentType::About), Err(_err) => false, }); // return about message stream @@ -279,13 +381,14 @@ impl Sbot { // 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; + 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 @@ -304,14 +407,20 @@ impl Sbot { } /// Get HashMap of profile info for given user - pub async fn get_profile_info(&mut self, ssb_id: &str) -> Result, GolgiError> { + 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> { + 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 } @@ -330,7 +439,7 @@ impl Sbot { pub async fn get_about_info( &mut self, ssb_id: &str, - mut keys_to_search_for: Vec<&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?; @@ -342,7 +451,7 @@ impl Sbot { 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 + break; } // if there are still keys we are looking for, then continue searching match res { @@ -350,7 +459,9 @@ impl Sbot { // 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) + let option_val = msg + .content + .get(key) .and_then(|val| val.as_str()) .map(|val| val.to_string()); match option_val { @@ -360,15 +471,13 @@ impl Sbot { // 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 - } + None => continue, } } } Err(err) => { // skip errors - continue + continue; } } }