diff --git a/examples/ssb-client.rs b/examples/ssb-client.rs index 2a50bff..3c832e4 100644 --- a/examples/ssb-client.rs +++ b/examples/ssb-client.rs @@ -1,6 +1,6 @@ use std::process; -use kuska_ssb::api::dto::content::Post; +use kuska_ssb::api::dto::content::TypedMessage; use golgi::error::GolgiError; use golgi::sbot::Sbot; @@ -11,14 +11,27 @@ async fn run() -> Result<(), GolgiError> { let id = sbot_client.whoami().await?; println!("{}", id); - let post = Post::new( - "glyph from golgi".to_string(), - // mentions - None, - ); + let name = TypedMessage::About { + about: id, + name: Some("golgi".to_string()), + title: None, + branch: None, + image: None, + description: None, + location: None, + start_datetime: None, + }; - let msg_ref = sbot_client.publish_post(post).await?; - println!("{}", msg_ref); + let name_msg_ref = sbot_client.publish(name).await?; + println!("{}", name_msg_ref); + + let post = TypedMessage::Post { + text: "golgi go womp womp".to_string(), + mentions: None, + }; + + let post_msg_ref = sbot_client.publish(post).await?; + println!("{}", post_msg_ref); Ok(()) } diff --git a/src/error.rs b/src/error.rs index 9ff3ef8..fd9c85f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,37 +1,40 @@ -/* -use async_std::{io::Read, net::TcpStream}; -use std::fmt::Debug; +//! Custom error type for `golgi`. -use kuska_handshake::async_std::BoxStream; -use kuska_sodiumoxide::crypto::sign::ed25519; -use kuska_ssb::api::{dto::WhoAmIOut, ApiCaller}; -use kuska_ssb::discovery; -use kuska_ssb::keystore; -use kuska_ssb::keystore::OwnedIdentity; -use kuska_ssb::rpc::{RecvMsg, RequestNo, RpcReader, RpcWriter}; +use std::io::Error as IoError; -type Result = std::result::Result>; - -pub fn whoami_res_parse(body: &[u8]) -> Result { - Ok(serde_json::from_slice(body)?) -} -*/ +use base64::DecodeError; +use kuska_handshake::async_std::Error as HandshakeError; +use kuska_ssb::api::Error as ApiError; +use kuska_ssb::feed::Error as FeedError; +use kuska_ssb::rpc::Error as RpcError; +use serde_json::Error as JsonError; +/// A custom error type encapsulating all possible errors for this library. +/// `From` implementations are provided for external error types, allowing +/// the `?` operator to be used on functions which return `Result<_, GolgiError>`. #[derive(Debug)] pub enum GolgiError { - DecodeBase64(base64::DecodeError), + /// Failed to decode base64. + DecodeBase64(DecodeError), + /// IO error with context. Io { - source: std::io::Error, + /// The underlying IO error. + source: IoError, + /// Description of the error context. context: String, }, - Handshake(kuska_handshake::async_std::Error), - KuskaApi(kuska_ssb::api::Error), - KuskaFeed(kuska_ssb::feed::Error), - KuskaRpc(kuska_ssb::rpc::Error), - // error message returned from the go-sbot + /// Scuttlebutt secret handshake error. + Handshake(HandshakeError), + /// Kuska SSB API error. + Api(ApiError), + /// Kuska SSB feed error. + Feed(FeedError), + /// Kuska SSB RPC error. + Rpc(RpcError), + /// Go-sbot error. Sbot(String), - SerdeJson(serde_json::Error), - WhoAmI(String), + /// JSON serialization or deserialization error. + SerdeJson(JsonError), } impl std::error::Error for GolgiError { @@ -40,12 +43,11 @@ impl std::error::Error for GolgiError { GolgiError::DecodeBase64(ref err) => Some(err), GolgiError::Io { ref source, .. } => Some(source), GolgiError::Handshake(_) => None, - GolgiError::KuskaApi(ref err) => Some(err), - GolgiError::KuskaFeed(ref err) => Some(err), - GolgiError::KuskaRpc(ref err) => Some(err), + GolgiError::Api(ref err) => Some(err), + GolgiError::Feed(ref err) => Some(err), + GolgiError::Rpc(ref err) => Some(err), GolgiError::Sbot(_) => None, GolgiError::SerdeJson(ref err) => Some(err), - GolgiError::WhoAmI(_) => None, } } } @@ -57,50 +59,50 @@ impl std::fmt::Display for GolgiError { 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::KuskaApi(_) => write!(f, "SSB API failure"), - GolgiError::KuskaFeed(_) => write!(f, "SSB feed error"), + 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 // then have the core display msg be: "SSB RPC error: {}", context - GolgiError::KuskaRpc(_) => write!(f, "SSB RPC failure"), + GolgiError::Rpc(ref err) => write!(f, "SSB RPC failure: {}", err), GolgiError::Sbot(ref err) => write!(f, "Sbot returned an error response: {}", err), GolgiError::SerdeJson(_) => write!(f, "Failed to serialize JSON slice"), - GolgiError::WhoAmI(ref err) => write!(f, "{}", err), + //GolgiError::WhoAmI(ref err) => write!(f, "{}", err), } } } -impl From for GolgiError { - fn from(err: base64::DecodeError) -> Self { +impl From for GolgiError { + fn from(err: DecodeError) -> Self { GolgiError::DecodeBase64(err) } } -impl From for GolgiError { - fn from(err: kuska_handshake::async_std::Error) -> Self { +impl From for GolgiError { + fn from(err: HandshakeError) -> Self { GolgiError::Handshake(err) } } -impl From for GolgiError { - fn from(err: kuska_ssb::api::Error) -> Self { - GolgiError::KuskaApi(err) +impl From for GolgiError { + fn from(err: ApiError) -> Self { + GolgiError::Api(err) } } -impl From for GolgiError { - fn from(err: kuska_ssb::feed::Error) -> Self { - GolgiError::KuskaFeed(err) +impl From for GolgiError { + fn from(err: FeedError) -> Self { + GolgiError::Feed(err) } } -impl From for GolgiError { - fn from(err: kuska_ssb::rpc::Error) -> Self { - GolgiError::KuskaRpc(err) +impl From for GolgiError { + fn from(err: RpcError) -> Self { + GolgiError::Rpc(err) } } -impl From for GolgiError { - fn from(err: serde_json::Error) -> Self { +impl From for GolgiError { + fn from(err: JsonError) -> Self { GolgiError::SerdeJson(err) } } diff --git a/src/lib.rs b/src/lib.rs index 655cb88..6fd638d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,28 @@ -// #![warn(missing_docs)] +#![warn(missing_docs)] + +//! # 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._ +//! +//! ----- +//! +//! 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. +//! +//! ## Example Usage +//! +//! ```rust +//! use golgi::GolgiError; +//! use golgi::sbot::Sbot; +//! +//! pub async fn run() -> Result<(), GolgiError> { +//! let mut sbot_client = Sbot::init(None, None).await?; +//! +//! let id = sbot_client.whoami().await?; +//! println!("{}", id); +//! +//! Ok(()) +//! } +//! ``` pub mod error; pub mod sbot; diff --git a/src/sbot.rs b/src/sbot.rs index f1b892b..151071d 100644 --- a/src/sbot.rs +++ b/src/sbot.rs @@ -1,10 +1,16 @@ +//! Sbot type and associated methods. + use async_std::net::TcpStream; use kuska_handshake::async_std::BoxStream; use kuska_sodiumoxide::crypto::{auth, sign::ed25519}; use kuska_ssb::{ api::{ - dto::{content::Post, CreateHistoryStreamIn}, + dto::{ + //content::{About, Post}, + content::TypedMessage, + CreateHistoryStreamIn, + }, ApiCaller, }, discovery, keystore, @@ -15,6 +21,8 @@ use kuska_ssb::{ use crate::error::GolgiError; use crate::utils; +/// The Scuttlebutt identity, keys and configuration parameters for connecting to a local sbot +/// instance, as well as handles for calling RPC methods and receiving responses. pub struct Sbot { id: String, public_key: ed25519::PublicKey, @@ -27,6 +35,9 @@ 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 init(ip_port: Option, net_id: Option) -> Result { let address; if ip_port.is_none() { @@ -80,6 +91,7 @@ impl Sbot { }) } + /// Call the `whoami` RPC method and return an `id`. pub async fn whoami(&mut self) -> Result { let req_id = self.client.whoami_req_send().await?; @@ -88,15 +100,32 @@ impl Sbot { .map(|whoami| whoami.id) } + /// Call the `publish` RPC method and return a message reference. + /// + /// # Arguments + /// + /// * `msg` - A `TypedMessage` `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: TypedMessage) -> Result { + let req_id = self.client.publish_req_send(msg).await?; + + utils::get_async(&mut self.rpc_reader, req_id, utils::publish_res_parse).await + } + + /* pub async fn publish_post(&mut self, post: Post) -> Result { let req_id = self.client.publish_req_send(post).await?; utils::get_async(&mut self.rpc_reader, req_id, utils::publish_res_parse).await } + */ + /// Call the `createHistoryStream` RPC method and print the output. async fn create_history_stream(&mut self, id: String) -> Result<(), GolgiError> { let args = CreateHistoryStreamIn::new(id); let req_id = self.client.create_history_stream_req_send(&args).await?; + // TODO: we should return a vector of messages instead of printing them utils::print_source_until_eof(&mut self.rpc_reader, req_id, utils::feed_res_parse).await } }