Merge pull request 'Rename sbot module and improve documentation' (#27) from docs_review into main
Reviewed-on: #27
This commit is contained in:
commit
5099b56ecb
|
@ -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"
|
58
README.md
58
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(())
|
||||
}
|
||||
```
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<SsbMessageValue> 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<SsbMessageValue> = 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);
|
||||
}
|
||||
}
|
|
@ -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<SsbMessageValue, GolgiError>`.
|
||||
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<SsbMessageValue>` using
|
||||
// `try_collect`. A `GolgiError` will be returned from the `run`
|
||||
// function if any element contains an error.
|
||||
let results: Vec<SsbMessageValue> = 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);
|
||||
}
|
||||
}
|
|
@ -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<impl Stream<Item = Result<SsbMessageValue, GolgiError>>, 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<Option<String>, 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<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>
|
||||
let latest_about_value = latest_about_message.and_then(|msg| {
|
||||
msg
|
||||
// SsbMessageValue -> Option<&Value>
|
||||
.content
|
||||
.get(key)
|
||||
// Option<&Value> -> <Option<&str>
|
||||
.and_then(|value| value.as_str())
|
||||
// Option<&str> -> Option<String>
|
||||
.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<HashMap<String, String>, 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<HashMap<String, String>, 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<HashMap<String, String>, 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<String, String> = 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<Option<String>, 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<Option<String>, GolgiError> {
|
||||
self.get_latest_about_message(ssb_id, "description").await
|
||||
}
|
||||
}
|
|
@ -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<String, GolgiError> {
|
||||
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<String, GolgiError> {
|
||||
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<String, GolgiError> {
|
||||
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<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
|
||||
}
|
||||
|
||||
/// 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<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
|
||||
}
|
||||
|
||||
/// 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<Vec<String>, 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<Vec<String>, 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<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,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
|
@ -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<SubsetQueryOptions>,
|
||||
) -> Result<impl Stream<Item = Result<SsbMessageValue, GolgiError>>, 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)
|
||||
}
|
||||
}
|
|
@ -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<impl Stream<Item = Result<SsbMessageValue, GolgiError>>, 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)
|
||||
}
|
||||
}
|
|
@ -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<String, GolgiError> {
|
||||
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<String, GolgiError> {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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::*;
|
|
@ -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<String, GolgiError> {
|
||||
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<String, GolgiError> {
|
||||
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<String, GolgiError> {
|
||||
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<String, GolgiError> {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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<String, GolgiError> {
|
||||
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())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
57
src/lib.rs
57
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};
|
||||
|
|
|
@ -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<SsbMessageContentType, GolgiError> {
|
||||
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<SsbMessageContent, GolgiError> {
|
||||
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)]
|
||||
|
|
|
@ -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<TcpStream>,
|
||||
/// RpcReader object for reading responses from go-sbot
|
||||
pub rpc_reader: RpcReader<TcpStream>,
|
||||
}
|
||||
|
||||
/// 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<String>,
|
||||
net_id: Option<String>,
|
||||
) -> Result<Sbot, GolgiError> {
|
||||
/// 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<String>, net_id: Option<String>) -> Result<Sbot, GolgiError> {
|
||||
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<SbotConnection, GolgiError> {
|
||||
|
@ -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<String, GolgiError> {
|
||||
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())
|
||||
}
|
||||
}
|
558
src/sbot.rs_bak
558
src/sbot.rs_bak
|
@ -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<TcpStream>,
|
||||
rpc_reader: RpcReader<TcpStream>,
|
||||
}
|
||||
|
||||
/// 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<String>, net_id: Option<String>) -> Result<Sbot, GolgiError> {
|
||||
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<SbotConnection, GolgiError> {
|
||||
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<SbotConnection, GolgiError> {
|
||||
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<SsbMessageValue, GolgiError>
|
||||
///
|
||||
/// # 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<SubsetQueryOptions>,
|
||||
) -> Result<impl Stream<Item = Result<SsbMessageValue, GolgiError>>, 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<String, GolgiError> {
|
||||
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<String, GolgiError> {
|
||||
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<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,
|
||||
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,
|
||||
)
|
||||
.await
|
||||
}
|
||||
*/
|
||||
|
||||
/// Call the `invite create` RPC method and return the created invite
|
||||
pub async fn invite_create(&mut self, uses: u16) -> Result<String, GolgiError> {
|
||||
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<String, GolgiError> {
|
||||
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<String, GolgiError> {
|
||||
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<String, GolgiError> {
|
||||
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<String, GolgiError> {
|
||||
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<impl Stream<Item = Result<SsbMessageValue, GolgiError>>, 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<Option<String>, 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<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>
|
||||
let latest_about_value = latest_about_message.and_then(|msg| {
|
||||
msg
|
||||
// SsbMessageValue -> Option<&Value>
|
||||
.content
|
||||
.get(key)
|
||||
// Option<&Value> -> <Option<&str>
|
||||
.and_then(|value| value.as_str())
|
||||
// Option<&str> -> Option<String>
|
||||
.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<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> {
|
||||
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<HashMap<String, String>, 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<String, String> = 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<Option<String>, 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<Option<String>, GolgiError> {
|
||||
self.get_latest_about_message(ssb_id, "description").await
|
||||
}
|
||||
|
||||
/// Call the `createHistoryStream` RPC method
|
||||
/// and return a Stream of Result<SsbMessageValue, GolgiError>
|
||||
pub async fn create_history_stream(
|
||||
&mut self,
|
||||
id: String,
|
||||
) -> Result<impl Stream<Item = Result<SsbMessageValue, GolgiError>>, 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)
|
||||
}
|
||||
}
|
|
@ -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<impl Stream<Item = Result<SsbMessageValue, GolgiError>>, 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<Option<String>, 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<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>
|
||||
let latest_about_value = latest_about_message.and_then(|msg| {
|
||||
msg
|
||||
// SsbMessageValue -> Option<&Value>
|
||||
.content
|
||||
.get(key)
|
||||
// Option<&Value> -> <Option<&str>
|
||||
.and_then(|value| value.as_str())
|
||||
// Option<&str> -> Option<String>
|
||||
.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<HashMap<String, String>, 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<HashMap<String, String>, 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<HashMap<String, String>, 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<String, String> = 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<Option<String>, 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<Option<String>, GolgiError> {
|
||||
self.get_latest_about_message(ssb_id, "description").await
|
||||
}
|
||||
}
|
|
@ -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<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,
|
||||
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
|
||||
}
|
||||
|
||||
/// Return 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,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
|
@ -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<SsbMessageValue, GolgiError>
|
||||
///
|
||||
/// # 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<SubsetQueryOptions>,
|
||||
) -> Result<impl Stream<Item = Result<SsbMessageValue, GolgiError>>, 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)
|
||||
}
|
||||
}
|
|
@ -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<SsbMessageValue, GolgiError>.
|
||||
pub async fn create_history_stream(
|
||||
&mut self,
|
||||
id: String,
|
||||
) -> Result<impl Stream<Item = Result<SsbMessageValue, GolgiError>>, 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)
|
||||
}
|
||||
}
|
|
@ -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<String, GolgiError> {
|
||||
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<String, GolgiError> {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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<String, GolgiError> {
|
||||
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<String, GolgiError> {
|
||||
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<String, GolgiError> {
|
||||
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<String, GolgiError> {
|
||||
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
|
||||
}
|
||||
}
|
69
src/utils.rs
69
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<SsbMessageKVT, GolgiError> {
|
|||
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<String, GolgiError> {
|
|||
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<Value, GolgiError> {
|
|||
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<SsbMessageValue, GolgiError>
|
|||
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<T, GolgiError>.
|
||||
/// 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<T, GolgiError>`. 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<R>,
|
||||
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<T, GolgiError>.
|
||||
/// 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<T, GolgiError>`. 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<R>,
|
||||
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<T, GolgiError>.
|
||||
/// 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<T, GolgiError>`. 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<R>,
|
||||
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<T, GolgiError>.
|
||||
/// 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<T, GolgiError>`. 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<TcpStream>,
|
||||
req_no: RequestNo,
|
||||
|
|
Loading…
Reference in New Issue