//! # peach-sbotcli //! //! Rust wrapper around the Go `sbotcli` ScuttleButt tool ([cryptoscope/ssb](https://github.com/cryptoscope/ssb)), allowing interaction with a `gosbot` instance. //! //! ## Example //! //! ```rust //! use peach_sbotcli::{Sbot, SbotCliError}; //! //! fn example() -> Result<(), SbotCliError> { //! // uses default paths for `sbotcli` and `go-sbot` working directory //! let sbot = Sbot::init(None, None)?; //! //! let id = "@p13zSAiOpguI9nsawkGijsnMfWmFd5rlUNpzekEE+vI=.ed25519"; //! //! let follow_ref = sbot.follow(id)?; //! let block_ref = sbot.block(id)?; //! //! let invite_code = sbot.create_invite()?; //! //! Ok(()) //! } //! ``` //! //! ## Documentation //! //! Use `cargo doc` to generate and serve the Rust documentation for this library: //! //! ```bash //! git clone https://git.coopcloud.tech/PeachCloud/peach-sbotcli.git //! cd peach-sbotcli //! cargo doc --no-deps --open //! ``` //! //! ## License //! //! AGPL-3.0. pub mod error; mod utils; pub use crate::error::SbotCliError; 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, } 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 { // 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` 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(), }) } /* 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 { 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))) } } /* 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 { 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 ))) } } /// 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 { 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 ))) } } /* GET NAME */ /// 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, 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 ))) } } /* 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 { 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 ))) } } /// 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 { 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 ))) } } /* 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 { 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 ))) } } // 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 { 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 ))) } } /// 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) /// * `id` - A string slice representing an id (profile reference / public key) /// pub fn publish_name(&self, id: &str, name: &str) -> Result { 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 ))) } } /// 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 { 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 { 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 ))) } } /* 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. /// pub fn whoami(&self) -> Result, SbotCliError> { 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)?; Ok(id) } else { let stderr = std::str::from_utf8(&output.stderr)?; Err(SbotCliError::WhoAmI(format!( "Error calling whoami: {}", stderr ))) } } } #[cfg(test)] mod tests { #[test] fn it_works() { let result = 2 + 2; assert_eq!(result, 4); } }