//! Define peer relationships and query the social graph. //! //! Implements the following methods: //! //! - [`Sbot::block`] //! - [`Sbot::unblock`] //! - [`Sbot::follow`] //! - [`Sbot::unfollow`] //! - [`Sbot::friends_hops`] //! - [`Sbot::friends_is_blocking`] //! - [`Sbot::friends_is_following`] //! - [`Sbot::get_blocks`] //! - [`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`. /// /// # Example /// /// ```rust /// use golgi::{Sbot, GolgiError, sbot::Keystore}; /// /// async fn follow_peer() -> Result<(), GolgiError> { /// let mut sbot_client = Sbot::init(Keystore::Patchwork, 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, Some(true), None).await } /// Unfollow a peer. /// /// This is a convenience method to publish a contact message with /// following: `false`. /// /// # Example /// /// ```rust /// use golgi::{Sbot, GolgiError, sbot::Keystore}; /// /// async fn unfollow_peer() -> Result<(), GolgiError> { /// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?; /// /// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519"; /// /// match sbot_client.unfollow(ssb_id).await { /// Ok(msg_ref) => { /// println!("unfollow msg reference is: {}", msg_ref) /// }, /// Err(e) => eprintln!("failed to unfollow {}: {}", ssb_id, e) /// } /// /// Ok(()) /// } /// ``` pub async fn unfollow(&mut self, contact: &str) -> Result { self.set_relationship(contact, Some(false), None).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, sbot::Keystore}; /// /// async fn block_peer() -> Result<(), GolgiError> { /// let mut sbot_client = Sbot::init(Keystore::Patchwork, 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 { // we want to unfollow and block self.set_relationship(contact, Some(false), Some(true)) .await } /// Unblock a peer. /// /// This is a convenience method to publish a contact message with /// blocking: `false`. /// /// # Example /// /// ```rust /// use golgi::{Sbot, GolgiError, sbot::Keystore}; /// /// async fn unblock_peer() -> Result<(), GolgiError> { /// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?; /// /// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519"; /// /// match sbot_client.unblock(ssb_id).await { /// Ok(msg_ref) => { /// println!("unblock msg reference is: {}", msg_ref) /// }, /// Err(e) => eprintln!("failed to unblock {}: {}", ssb_id, e) /// } /// /// Ok(()) /// } /// ``` pub async fn unblock(&mut self, contact: &str) -> Result { self.set_relationship(contact, None, Some(false)).await } /// Publish a contact message defining the relationship for a peer. /// /// # Example /// /// ```rust /// use golgi::{Sbot, GolgiError, sbot::Keystore}; /// /// async fn relationship() -> Result<(), GolgiError> { /// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?; /// /// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519"; /// let following = Some(true); /// // Could also be `None` to only publish the following relationship. /// let blocking = Some(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: Option, blocking: Option, ) -> Result { let msg = SsbMessageContent::Contact { contact: Some(contact.to_string()), following, 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, sbot::Keystore}; /// /// async fn relationship() -> Result<(), GolgiError> { /// let mut sbot_client = Sbot::init(Keystore::Patchwork, 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, sbot::Keystore}; /// /// async fn relationship() -> Result<(), GolgiError> { /// let mut sbot_client = Sbot::init(Keystore::Patchwork, 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 blocked by the local peer. /// /// # Example /// /// ```rust /// use golgi::{Sbot, GolgiError, sbot::Keystore}; /// /// async fn follows() -> Result<(), GolgiError> { /// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?; /// /// let follows = sbot_client.get_blocks().await?; /// /// if follows.is_empty() { /// println!("we do not block any peers") /// } else { /// follows.iter().for_each(|peer| println!("we block {}", peer)) /// } /// /// Ok(()) /// } /// ``` pub async fn get_blocks(&mut self) -> Result, GolgiError> { let mut sbot_connection = self.get_sbot_connection().await?; let req_id = sbot_connection.client.friends_blocks_req_send().await?; utils::get_source_until_eof( &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, sbot::Keystore}; /// /// async fn follows() -> Result<(), GolgiError> { /// let mut sbot_client = Sbot::init(Keystore::Patchwork, 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: 0, 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, sbot::Keystore}; /// /// async fn followers() -> Result<(), GolgiError> { /// let mut sbot_client = Sbot::init(Keystore::Patchwork, 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: 0, 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. /// /// Hops = 0 returns a list of peers followed by the local identity. /// Those peers may or may not be mutual follows (ie. friends). /// /// Hops = 1 includes the peers followed by the peers we follow. /// For example, if the local identity follows Aiko and Aiko follows /// Bridgette and Chris, hops = 1 will return a list including the public /// keys for Aiko, Bridgette and Chris (even though Bridgette and Chris /// are not followed by the local identity). /// /// When reverse = True, hops = 0 should return a list of peers who /// follow the local identity, ie. followers (but this is not currently /// implemented in go-sbot). /// /// # Example /// /// ```rust /// use golgi::{Sbot, GolgiError, api::friends::FriendsHops, sbot::Keystore}; /// /// async fn peers_within_range() -> Result<(), GolgiError> { /// let mut sbot_client = Sbot::init(Keystore::Patchwork, 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 } }