Merge pull request 'Rename sbot module and improve documentation' (#27) from docs_review into main

Reviewed-on: #27
This commit is contained in:
glyph 2022-02-15 08:34:51 +00:00
commit 5099b56ecb
31 changed files with 1756 additions and 1411 deletions

View File

@ -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"

View File

@ -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(())
}
```

98
examples/basic.rs Normal file
View File

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

122
examples/friends.rs Normal file
View File

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

76
examples/invite.rs Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

125
examples/streams.rs Normal file
View File

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

385
src/api/about.rs Normal file
View File

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

326
src/api/friends.rs Normal file
View File

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

86
src/api/get_subset.rs Normal file
View File

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

63
src/api/history_stream.rs Normal file
View File

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

79
src/api/invite.rs Normal file
View File

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

10
src/api/mod.rs Normal file
View File

@ -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::*;

154
src/api/publish.rs Normal file
View File

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

45
src/api/whoami.rs Normal file
View File

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

View File

@ -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

View File

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

View File

@ -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)]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,