Add methods for following, blocking and looking up peers #18

Merged
glyph merged 8 commits from friends-requests into sbot-connection 2022-02-04 08:31:02 +00:00
3 changed files with 228 additions and 25 deletions

86
examples/ssb-friends.rs Normal file
View File

@ -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);
}
}

1
git_hooks/pre-commit Executable file
View File

@ -0,0 +1 @@
cargo fmt

View File

@ -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
@ -39,7 +41,7 @@ pub struct Sbot {
private_key: ed25519::SecretKey,
address: String,
// aka caps key (scuttleverse identifier)
network_id: auth::Key
network_id: auth::Key,
}
impl Sbot {
@ -68,7 +70,7 @@ impl Sbot {
public_key: pk,
private_key: sk,
address,
network_id
network_id,
})
}
@ -136,8 +138,12 @@ 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)
}
@ -180,6 +186,111 @@ impl Sbot {
.await
}
// Convenience method to set a relationship with following: true, blocking: false
pub async fn follow(&mut self, contact: &str) -> Result<String, GolgiError> {
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<String, GolgiError> {
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<String, GolgiError> {
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,
Review

I'm a bit puzzled by the utility of this method. Could you help me understand? Why not just have two functions: follow() and unfollow()?

I'm a bit puzzled by the utility of this method. Could you help me understand? Why not just have two functions: `follow()` and `unfollow()`?
Review

@glyph friends.follow is one of the muxrpc methods defined in https://github.com/ssbc/ssb-friends. I thought we could definite it here in the same way so there is some symmetry between the golgi api and the js side.

It seems that friends.follow is not currently implemented in go-sbot anyway,
so I would actually just remove this function for now anyway,
and use the set_relationsip function I wrote above (and we can also write follow and unfollow methods that call set_relationship).

In the future, in an ideal world, I think it could be a good idea for golgi to intentionally implement all of the muxrpc methods as part of its API, in the same way as they are in javascript, to make documentation clear and uniform, between both stacks,

even if golgi also then implements additional convenience methods that do things in other ways. But since go-sbot doesnt seem to yet implement all the same muxrpc calls as the js side, then this interface symmetry is not achievable for now anyway

@glyph friends.follow is one of the muxrpc methods defined in https://github.com/ssbc/ssb-friends. I thought we could definite it here in the same way so there is some symmetry between the golgi api and the js side. It seems that friends.follow is not currently implemented in go-sbot anyway, so I would actually just remove this function for now anyway, and use the set_relationsip function I wrote above (and we can also write follow and unfollow methods that call set_relationship). In the future, in an ideal world, I think it could be a good idea for golgi to intentionally implement all of the muxrpc methods as part of its API, in the same way as they are in javascript, to make documentation clear and uniform, between both stacks, even if golgi also then implements additional convenience methods that do things in other ways. But since go-sbot doesnt seem to yet implement all the same muxrpc calls as the js side, then this interface symmetry is not achievable for now anyway
Review

Thanks for explaining.

so I would actually just remove this function for now anyway,
and use the set_relationsip function I wrote above (and we can also write follow and unfollow methods that call set_relationship).

This sounds great to me. I really like your set_relationship function. It's so much clearer to me than friends.follow.

In the future, in an ideal world, I think it could be a good idea for golgi to intentionally implement all of the muxrpc methods as part of its API, in the same way as they are in javascript, to make documentation clear and uniform, between both stacks,

I'd love to discuss this in detail another time because I think it's a very important design decision. My brief opinion is that there are some ugly parts of the JS API that I really don't want to emulate just to achieve a 1:1 mapping. I'd rather have a beautiful, clear and consistent golgi API that deviated from JS in places.

One option might be to ask arj, staltz and mix for their opinion ("which of these methods are necessary in your opinion and which are best left out?"), since they're the ones with the most experience of the JS stack.

Thanks for explaining. > so I would actually just remove this function for now anyway, and use the set_relationsip function I wrote above (and we can also write follow and unfollow methods that call set_relationship). This sounds great to me. I really like your `set_relationship` function. It's so much clearer to me than `friends.follow`. > In the future, in an ideal world, I think it could be a good idea for golgi to intentionally implement all of the muxrpc methods as part of its API, in the same way as they are in javascript, to make documentation clear and uniform, between both stacks, I'd love to discuss this in detail another time because I think it's a very important design decision. My brief opinion is that there are some ugly parts of the JS API that I really don't want to emulate just to achieve a 1:1 mapping. I'd rather have a beautiful, clear and consistent golgi API that deviated from JS in places. One option might be to ask arj, staltz and mix for their opinion ("which of these methods are necessary in your opinion and which are best left out?"), since they're the ones with the most experience of the JS stack.
Review

hmm interesting, could see either way ~

I guess its mostly a question of exclusion

A - I could imagine a muxrpc file in golgi which simply includes a 1:1 golgi function for each muxrpc function,
even if most rust applications on top of golgi dont actually use all those calls

B - could intentionally leave out functions like friends.follow which we decide golgi doesn't want to support

hmm interesting, could see either way ~ I guess its mostly a question of exclusion A - I could imagine a muxrpc file in golgi which simply includes a 1:1 golgi function for each muxrpc function, even if most rust applications on top of golgi dont actually use all those calls B - could intentionally leave out functions like friends.follow which we decide golgi doesn't want to support
Review

re friends.follow not existing, just for reference: here's how sbotcli handles it: https://github.com/cryptoscope/ssb/blob/master/cmd/sbotcli/publish.go#L229

(you publish a contact message with following: true)

re friends.follow not existing, just for reference: here's how sbotcli handles it: https://github.com/cryptoscope/ssb/blob/master/cmd/sbotcli/publish.go#L229 (you publish a contact message with `following: true`)
args: RelationshipQuery,
) -> Result<String, GolgiError> {
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<String, GolgiError> {
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<String> where each element is a peer you are following
pub async fn get_follows(&mut self) -> Result<Vec<String>, GolgiError> {
self.friends_hops(FriendsHops {
max: 1,
start: None,
reverse: Some(false),
})
.await
}
// Gets a Vec<String> 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<Vec<String>, GolgiError> {
self.friends_hops(FriendsHops {
max: 1,
start: None,
reverse: Some(true),
})
.await
}
/// Call the `friends hops` RPC method and return a Vector<String>
/// 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<Vec<String>, 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,
Review

As far as I can tell by looking at the Go codecase, the reverse parameter is not supported for this RPC call. I think it might be best to keep such calls out of the public API of golgi to avoid any confusion, at least until some future time when either A) we learn that the parameter is actually supported but not working for some reason, or B) parameter support is added.

As far as I can tell by looking at the Go codecase, the `reverse` parameter is not supported for this RPC call. I think it might be best to keep such calls out of the public API of golgi to avoid any confusion, at least until some future time when either A) we learn that the parameter is actually supported but not working for some reason, or B) parameter support is added.
Review

that sounds like a good plan - in this PR I was mostly just trying to document everything I tried so we could see the state of what's missing.

the missing reverse parameter is a particularly big missing feature, since I'm not sure how we can look up someone's followers without it.

Maybe I could modify this function to return an empty list for now,
with a todo next to it,
and then we could still call this function in the PeachPub UI for the routes that need it, knowing that eventually it will be implemented,
but for now it cannot be due to a limitation of go-sbot.

Could also remove the function entirely, but it does seem like this is pretty much the function we would want, at some point.

that sounds like a good plan - in this PR I was mostly just trying to document everything I tried so we could see the state of what's missing. the missing reverse parameter is a particularly big missing feature, since I'm not sure how we can look up someone's followers without it. Maybe I could modify this function to return an empty list for now, with a todo next to it, and then we could still call this function in the PeachPub UI for the routes that need it, knowing that eventually it will be implemented, but for now it cannot be due to a limitation of go-sbot. Could also remove the function entirely, but it does seem like this is pretty much the function we would want, at some point.
Review

Oh yeah I just meant keeping it out of the public API (no pub before fn...) so that it's still all there but not available to a library user.

Oh yeah I just meant keeping it out of the public API (no `pub` before `fn...`) so that it's still all there but not available to a library user.
Review

hmm since we actually will want to use this function to write the Peers route in peach-web, we would need it to be pub for that. But for now I'll removed the pub.

hmm since we actually will want to use this function to write the Peers route in peach-web, we would need it to be pub for that. But for now I'll removed the pub.
)
.await
}
/// Wrapper for publish which constructs and publishes a post message appropriately from a string.
///
/// # Arguments
@ -252,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
@ -272,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<Result<SsbMessageValue, GolgiError>> = 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<Result<SsbMessageValue, GolgiError>> =
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<Result<SsbMessageValue, GolgiError>> -> Option<SsbMessageValue>
let latest_about_message = latest_about_message_res.and_then(|msg| msg.ok());
// Option<SsbMessageValue> -> Option<String>
@ -297,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<HashMap<String, String>, GolgiError> {
pub async fn get_profile_info(
&mut self,
ssb_id: &str,
) -> Result<HashMap<String, String>, 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<HashMap<String, String>, GolgiError> {
pub async fn get_name_and_image(
&mut self,
ssb_id: &str,
) -> Result<HashMap<String, String>, GolgiError> {
let mut keys_to_search_for = vec!["name", "image"];
self.get_about_info(ssb_id, keys_to_search_for).await
}
@ -323,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<HashMap<String, String>, GolgiError> {
// get about_message_stream
let about_message_stream = self.get_about_message_stream(ssb_id).await?;
@ -335,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 {
@ -343,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 {
@ -353,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;
}
}
}