2021-11-05 11:46:35 +00:00
//! # go-sbotcli-rs
2021-11-03 13:52:35 +00:00
//!
//! Rust wrapper around the Go `sbotcli` ScuttleButt tool ([cryptoscope/ssb](https://github.com/cryptoscope/ssb)), allowing interaction with a `gosbot` instance.
//!
//! ## Example
//!
//! ```rust
2021-11-05 11:46:35 +00:00
//! use go_sbotcli_rs::{Sbot, SbotCliError};
2021-11-03 13:52:35 +00:00
//!
//! fn example() -> Result<(), SbotCliError> {
2021-11-04 14:57:27 +00:00
//! // uses default paths for `sbotcli` and `go-sbot` working directory
//! let sbot = Sbot::init(None, None)?;
//!
2021-11-03 13:52:35 +00:00
//! let id = "@p13zSAiOpguI9nsawkGijsnMfWmFd5rlUNpzekEE+vI=.ed25519";
//!
2021-11-04 14:57:27 +00:00
//! let follow_ref = sbot.follow(id)?;
//! let block_ref = sbot.block(id)?;
2021-11-03 13:52:35 +00:00
//!
2021-11-04 14:57:27 +00:00
//! let invite_code = sbot.create_invite()?;
2021-11-03 13:52:35 +00:00
//!
//! Ok(())
//! }
//! ```
//!
//! ## Documentation
//!
//! Use `cargo doc` to generate and serve the Rust documentation for this library:
//!
//! ```bash
2021-11-05 11:46:35 +00:00
//! git clone https://git.coopcloud.tech/PeachCloud/go-sbotcli-rs.git
2021-11-03 13:52:35 +00:00
//! cd peach-sbotcli
//! cargo doc --no-deps --open
//! ```
//!
//! ## License
//!
2021-11-05 11:46:35 +00:00
//! LGPL-3.0.
2021-11-03 13:52:35 +00:00
pub mod error ;
mod utils ;
pub use crate ::error ::SbotCliError ;
2021-11-04 14:57:27 +00:00
use std ::{ ffi ::OsString , path ::PathBuf , process ::Command , result ::Result } ;
/// An `sbotcli` instance with associated methods for querying a Go sbot server.
pub struct Sbot {
/// The path to the `sbotcli` binary.
pub sbotcli_path : OsString ,
/// The working directory of the `go-sbot` instance (where the Scuttlebutt database is stored).
pub sbot_working_dir : OsString ,
}
2021-11-03 13:52:35 +00:00
2021-11-04 14:57:27 +00:00
impl Sbot {
/// Initialise an `sbotcli` instance. Sets default path parameters for the `sbotcli` binary and
/// `go-sbot` working directory if none are provided. Alternatively, uses the provided
/// parameter(s) to define the path(s).
///
/// # Arguments
///
/// * `sbotcli_path` - an optional string slice representing a file path
/// * `sbot_working_dir` - an optional string slice representing a directory path
///
pub fn init (
sbotcli_path : Option < & str > ,
sbot_working_dir : Option < & str > ,
) -> Result < Self , SbotCliError > {
// set default path for sbotcli
let mut path = PathBuf ::from ( r "/usr/bin/sbotcli" ) ;
// overwrite the default path if one has been provided via the `sbotcli_path` arg
if let Some ( p ) = sbotcli_path {
path = PathBuf ::from ( p ) ;
} ;
let mut dir = PathBuf ::new ( ) ;
if let Some ( d ) = sbot_working_dir {
// define the `sbot_working_dir` using the provided arg
dir . push ( d )
} else {
// set default path for go-sbot working directory if no arg is provided
// returns `Option<PathBuf>`
let home_dir = dirs ::home_dir ( ) ;
// it's possible that the home directory cannot be determined, hence the `match`
match home_dir {
Some ( home_dir_path ) = > {
dir . push ( home_dir_path ) ;
dir . push ( " .ssb-go " ) ;
}
// return an error if the home directory could not be determined
None = > return Err ( SbotCliError ::NoHomeDir ) ,
}
} ;
Ok ( Self {
sbotcli_path : path . into_os_string ( ) ,
sbot_working_dir : dir . into_os_string ( ) ,
} )
2021-11-03 13:52:35 +00:00
}
2021-11-04 14:57:27 +00:00
/* BLOBS */
// TODO: file an issue
// - doesn't seem to be implemented in sbotcli yet
// - unsure of input type (`file_path`)
// - unsure about using `-` to open `stdin`
//
/// Add a file to the blob store.
///
/// Calls `sbotcli blobs add [file_path]`. On success: trims the trailing whitespace from `stdout` and returns the blob reference. On error: returns the `stderr` output with a description.
///
/// # Arguments
///
/// * `file_path` - A string slice representing a file path
///
pub fn add_blob ( & self , file_path : & str ) -> Result < String , SbotCliError > {
let output = Command ::new ( & self . sbotcli_path )
. arg ( " blobs " )
. arg ( " add " )
. arg ( file_path )
. output ( ) ? ;
if output . status . success ( ) {
let stdout = String ::from_utf8 ( output . stdout ) ? ;
let msg_ref = stdout . trim_end ( ) . to_string ( ) ;
Ok ( msg_ref )
} else {
let stderr = std ::str ::from_utf8 ( & output . stderr ) ? ;
Err ( SbotCliError ::Blob ( format! ( " Error adding blob: {} " , stderr ) ) )
}
2021-11-03 13:52:35 +00:00
}
2021-11-04 14:57:27 +00:00
/* CONTACTS */
/// Follow a peer.
///
/// Calls `sbotcli publish contact --following [id]`. On success: trims the trailing whitespace from `stdout`
/// and returns the message reference. On error: returns the `stderr` output with a description.
///
/// # Arguments
///
/// * `id` - A string slice representing an id (profile reference / public key)
///
pub fn follow ( & self , id : & str ) -> Result < String , SbotCliError > {
let output = Command ::new ( & self . sbotcli_path )
. arg ( " publish " )
. arg ( " contact " )
. arg ( " --following " )
. arg ( id )
. output ( ) ? ;
if output . status . success ( ) {
let stdout = String ::from_utf8 ( output . stdout ) ? ;
let msg_ref = stdout . trim_end ( ) . to_string ( ) ;
Ok ( msg_ref )
} else {
let stderr = std ::str ::from_utf8 ( & output . stderr ) ? ;
Err ( SbotCliError ::Contact ( format! (
" Error following peer: {} " ,
stderr
) ) )
}
2021-11-03 13:52:35 +00:00
}
2021-11-04 14:57:27 +00:00
/// Block a peer.
///
/// Calls `sbotcli publish contact --blocking [id]`. On success: trims the trailing whitespace from `stdout` and returns the message reference. On error: returns the `stderr` output with a description.
///
/// # Arguments
///
/// * `id` - A string slice representing an id (profile reference / public key)
///
pub fn block ( & self , id : & str ) -> Result < String , SbotCliError > {
let output = Command ::new ( & self . sbotcli_path )
. arg ( " publish " )
. arg ( " contact " )
. arg ( " --blocking " )
. arg ( id )
. output ( ) ? ;
if output . status . success ( ) {
let stdout = String ::from_utf8 ( output . stdout ) ? ;
let msg_ref = stdout . trim_end ( ) . to_string ( ) ;
Ok ( msg_ref )
} else {
let stderr = std ::str ::from_utf8 ( & output . stderr ) ? ;
Err ( SbotCliError ::Contact ( format! (
" Error blocking peer: {} " ,
stderr
) ) )
}
2021-11-03 13:52:35 +00:00
}
2021-11-15 11:23:48 +00:00
/* GET ABOUT MESSAGES */
2021-11-04 14:57:27 +00:00
/// Return latest name assignment from `about` msgs (the name in this case is for the public key
/// associated with the local sbot instance).
///
/// Calls `sbotcli bytype --limit 10 --reverse about`. On success: parses the `stdout` to extract the
/// `name` and returns it. On error: returns the `stderr` output with a description.
///
pub fn get_name ( & self ) -> Result < Option < String > , SbotCliError > {
let output = Command ::new ( & self . sbotcli_path )
. arg ( " bytype " )
. arg ( " --limit " )
. arg ( " 10 " )
. arg ( " --reverse " )
. arg ( " about " )
. output ( ) ? ;
if output . status . success ( ) {
let stdout = String ::from_utf8 ( output . stdout ) ? ;
let name = utils ::regex_finder ( r # ""name": "(.*)""# , & stdout ) ? ;
Ok ( name )
} else {
let stderr = std ::str ::from_utf8 ( & output . stderr ) ? ;
// TODO: create a more generic error variant
Err ( SbotCliError ::GetAboutMsgs ( format! (
" Error fetching about messages: {} " ,
stderr
) ) )
}
2021-11-03 13:52:35 +00:00
}
2021-11-15 11:23:48 +00:00
/// Return latest description assignment from `about` msgs
/// (the description associated with the public key of the local sbot instance).
///
/// Calls `sbotcli bytype --limit 10 --reverse about`. On success: parses the `stdout` to extract the
/// `description` and returns it. On error: returns the `stderr` output with a description.
///
pub fn get_description ( & self ) -> Result < Option < String > , SbotCliError > {
let output = Command ::new ( & self . sbotcli_path )
. arg ( " bytype " )
. arg ( " --limit " )
. arg ( " 10 " )
. arg ( " --reverse " )
. arg ( " about " )
. output ( ) ? ;
if output . status . success ( ) {
let stdout = String ::from_utf8 ( output . stdout ) ? ;
let description = utils ::regex_finder ( r # ""description": "(.*)""# , & stdout ) ? ;
Ok ( description )
} else {
let stderr = std ::str ::from_utf8 ( & output . stderr ) ? ;
// TODO: create a more generic error variant
Err ( SbotCliError ::GetAboutMsgs ( format! (
" Error fetching about messages: {} " ,
stderr
) ) )
}
}
2021-11-04 14:57:27 +00:00
/* INVITES */
/// Accept an invite code (trigger a mutual follow with the peer who generated the invite).
///
/// Calls `sbotcli invite accept [invite_code]`. On success: trims the trailing whitespace from `stdout` and returns the follow message reference. On error: returns the `stderr` output with a description.
///
/// # Arguments
///
/// * `invite` - A string slice representing an invite code
///
pub fn accept_invite ( & self , invite : & str ) -> Result < String , SbotCliError > {
let output = Command ::new ( & self . sbotcli_path )
. arg ( " invite " )
. arg ( " accept " )
. arg ( invite )
. output ( ) ? ;
if output . status . success ( ) {
let stdout = String ::from_utf8 ( output . stdout ) ? ;
let msg_ref = stdout . trim_end ( ) . to_string ( ) ;
Ok ( msg_ref )
} else {
let stderr = std ::str ::from_utf8 ( & output . stderr ) ? ;
Err ( SbotCliError ::Invite ( format! (
" Error accepting invite: {} " ,
stderr
) ) )
}
2021-11-03 13:52:35 +00:00
}
2021-11-04 14:57:27 +00:00
/// Return an invite code from go-sbot.
///
/// Calls `sbotcli invite create`. On success: trims the trailing whitespace from `stdout` and returns the invite code. On error: returns the `stderr` output with a description.
///
pub fn create_invite ( & self ) -> Result < String , SbotCliError > {
let output = Command ::new ( & self . sbotcli_path )
. arg ( " invite " )
. arg ( " create " )
. output ( ) ? ;
if output . status . success ( ) {
let stdout = String ::from_utf8 ( output . stdout ) ? ;
let invite = stdout . trim_end ( ) . to_string ( ) ;
Ok ( invite )
} else {
let stderr = std ::str ::from_utf8 ( & output . stderr ) ? ;
Err ( SbotCliError ::Invite ( format! (
" Error creating invite: {} " ,
stderr
) ) )
}
2021-11-03 13:52:35 +00:00
}
2021-11-04 14:57:27 +00:00
/* PUBLISH */
/// Publish an about message with an image.
///
/// Calls `sbotcli publish about --image [blob_reference] [id]`. On success: trims the trailing whitespace from `stdout` and returns the message reference. On error: returns the `stderr` output with a description.
///
/// # Arguments
///
/// * `blob_ref` - A string slice representing a blob reference
/// * `id` - A string slice representing an id (profile reference / public key)
///
pub fn publish_image ( & self , blob_ref : & str , id : & str ) -> Result < String , SbotCliError > {
let output = Command ::new ( & self . sbotcli_path )
. arg ( " publish " )
. arg ( " about " )
. arg ( " --image " )
. arg ( blob_ref )
. arg ( id )
. output ( ) ? ;
if output . status . success ( ) {
let stdout = String ::from_utf8 ( output . stdout ) ? ;
let msg_ref = stdout . trim_end ( ) . to_string ( ) ;
Ok ( msg_ref )
} else {
let stderr = std ::str ::from_utf8 ( & output . stderr ) ? ;
Err ( SbotCliError ::Publish ( format! (
" Error publishing image: {} " ,
stderr
) ) )
}
2021-11-03 13:52:35 +00:00
}
2021-11-04 14:57:27 +00:00
// TODO: file an issue
// - doesn't seem to be implemented in sbotcli yet
//
/// Publish an about message with a description.
///
/// Calls `sbotcli publish about --description [description] [id]`. On success: trims the trailing whitespace from `stdout` and returns the message reference. On error: returns the `stderr` output with a description.
///
/// # Arguments
///
/// * `description` - A string slice representing a profile description (bio)
/// * `id` - A string slice representing an id (profile reference / public key)
///
pub fn publish_description ( & self , description : & str , id : & str ) -> Result < String , SbotCliError > {
let output = Command ::new ( & self . sbotcli_path )
. arg ( " publish " )
. arg ( " about " )
. arg ( " --description " )
. arg ( description )
. arg ( id )
. output ( ) ? ;
if output . status . success ( ) {
let stdout = String ::from_utf8 ( output . stdout ) ? ;
let msg_ref = stdout . trim_end ( ) . to_string ( ) ;
Ok ( msg_ref )
} else {
let stderr = std ::str ::from_utf8 ( & output . stderr ) ? ;
Err ( SbotCliError ::Publish ( format! (
" Error publishing description: {} " ,
stderr
) ) )
}
2021-11-03 13:52:35 +00:00
}
2021-11-04 14:57:27 +00:00
/// Publish an about message with a name.
///
/// Calls `sbotcli publish about --name [name] [id]`. On success: trims the trailing whitespace from `stdout` and returns the message reference. On error: returns the `stderr` output with a description.
///
/// # Arguments
///
/// * `name` - A string slice representing a profile name (bio)
2021-11-15 13:15:06 +00:00
/// * `id` - A string slice representing the id (profile reference / public key)
/// of the profile being named
2021-11-04 14:57:27 +00:00
///
pub fn publish_name ( & self , id : & str , name : & str ) -> Result < String , SbotCliError > {
let output = Command ::new ( & self . sbotcli_path )
. arg ( " publish " )
. arg ( " about " )
. arg ( " --name " )
. arg ( name )
. arg ( id )
. output ( ) ? ;
if output . status . success ( ) {
let stdout = String ::from_utf8 ( output . stdout ) ? ;
let msg_ref = stdout . trim_end ( ) . to_string ( ) ;
Ok ( msg_ref )
} else {
let stderr = std ::str ::from_utf8 ( & output . stderr ) ? ;
Err ( SbotCliError ::Publish ( format! (
" Error publishing name: {} " ,
stderr
) ) )
}
2021-11-03 13:52:35 +00:00
}
2021-11-15 11:23:48 +00:00
/// Publish an about message with a name for one's own profile,
/// using whoami to find your own id.
///
/// Calls `sbotcli publish about --name [name] [self_id]`.
/// passing the id of the currently running sbot as self_id.
/// On success: trims the trailing whitespace from `stdout` and returns the message reference.
/// On error: returns the `stderr` output with a description.
///
/// # Arguments
///
/// * `name` - A string slice of the new name you would like to self-identify with
///
pub fn publish_own_name ( & self , name : & str ) -> Result < String , SbotCliError > {
let self_id = & self . whoami ( ) ? ;
self . publish_name ( name , self_id )
}
/// Publish an about message with a description for one's own profile,
/// using whoami to find your own id.
///
/// Calls `sbotcli publish about --description [description] [self_id]`.
/// passing the id of the currently running sbot as self_id.
/// On success: trims the trailing whitespace from `stdout` and returns the message reference.
/// On error: returns the `stderr` output with a description.
///
/// # Arguments
///
/// * `description` - A string slice of the description you would like to use
///
pub fn publish_own_description ( & self , description : & str ) -> Result < String , SbotCliError > {
let self_id = & self . whoami ( ) ? ;
self . publish_description ( description , self_id )
}
2021-11-04 14:57:27 +00:00
/// Publish a post (public message).
///
/// Calls `sbotcli publish post [text]". On success: trims the trailing whitespace from `stdout` and returns the message reference. On error: returns the `stderr` output with a description.
///
/// # Arguments
///
/// * `text` - A string slice representing a post (public message)
///
pub fn publish_post ( & self , text : & str ) -> Result < String , SbotCliError > {
let output = Command ::new ( & self . sbotcli_path )
. arg ( " publish " )
. arg ( " post " )
. arg ( text )
. output ( ) ? ;
if output . status . success ( ) {
let stdout = String ::from_utf8 ( output . stdout ) ? ;
let msg_ref = stdout . trim_end ( ) . to_string ( ) ;
Ok ( msg_ref )
} else {
let stderr = std ::str ::from_utf8 ( & output . stderr ) ? ;
Err ( SbotCliError ::Publish ( format! (
" Error publishing public post: {} " ,
stderr
) ) )
}
}
/// Publish a private message. Currently only supports sending the message to a single recipient
/// (multi-recipient support to be added later).
///
/// Calls `sbotcli publish post --recps [id] [msg]`. On success: trims the trailing whitespace from `stdout`
/// and returns the message reference. On error: returns the `stderr` output with a description.
///
/// # Arguments
///
/// * `id` - A string slice representing an id (profile reference / public key)
/// * `msg` - A string slice representing a private message
///
pub fn publish_private_message ( & self , id : & str , msg : & str ) -> Result < String , SbotCliError > {
let output = Command ::new ( & self . sbotcli_path )
. arg ( " publish " )
. arg ( " post " )
. arg ( " --recps " )
. arg ( id )
. arg ( msg )
. output ( ) ? ;
if output . status . success ( ) {
let stdout = String ::from_utf8 ( output . stdout ) ? ;
let msg_ref = stdout . trim_end ( ) . to_string ( ) ;
Ok ( msg_ref )
} else {
let stderr = std ::str ::from_utf8 ( & output . stderr ) ? ;
Err ( SbotCliError ::Publish ( format! (
" Error publishing private message: {} " ,
stderr
) ) )
}
2021-11-03 13:52:35 +00:00
}
2021-11-04 14:57:27 +00:00
/* WHOAMI */
/// Return the Scuttlebutt ID from go-sbot using `whoami`.
///
/// Calls `sbotcli call whoami`. On success: parses the `stdout` to extract the ID and returns it.
/// On error: returns the `stderr` output with a description.
///
2021-11-15 11:23:48 +00:00
pub fn whoami ( & self ) -> Result < String , SbotCliError > {
2021-11-04 14:57:27 +00:00
let output = Command ::new ( & self . sbotcli_path )
. arg ( " call " )
. arg ( " whoami " )
. output ( ) ? ;
if output . status . success ( ) {
let stdout = String ::from_utf8 ( output . stdout ) ? ;
let id = utils ::regex_finder ( r # ""id": "(.*)"\n"# , & stdout ) ? ;
2021-11-15 11:23:48 +00:00
match id {
// if the regex matches, then return the result
Some ( id ) = > {
Ok ( id )
} ,
// if the regex does not match, then return an error
None = > {
2021-11-15 13:15:06 +00:00
Err ( SbotCliError ::WhoAmI ( " Error calling whoami: failed to capture the id value using regex " . to_string ( ) ) )
2021-11-15 11:23:48 +00:00
}
}
2021-11-04 14:57:27 +00:00
} else {
let stderr = std ::str ::from_utf8 ( & output . stderr ) ? ;
Err ( SbotCliError ::WhoAmI ( format! (
" Error calling whoami: {} " ,
stderr
) ) )
}
2021-11-03 13:52:35 +00:00
}
}
#[ cfg(test) ]
mod tests {
#[ test ]
fn it_works ( ) {
let result = 2 + 2 ;
assert_eq! ( result , 4 ) ;
}
}