commit 97fc6cd3ff93d6c6228a2ae79f1748b7a00530af Author: mycognosist Date: Wed Nov 3 15:52:35 2021 +0200 docs, errors and basic api diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dc2273f --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +notes +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d8a2700 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "peach-sbotcli" +version = "0.1.0" +authors = ["Andrew Reid "] +edition = "2018" +description = "A wrapper around the Go sbotcli tool." +homepage = "https://peachcloud.org" +repository = "https://git.coopcloud.tech/PeachCloud/peach-sbotcli" +readme = "README.md" +license = "AGPL-3.0-only" + +[dependencies] +regex = "1.5.4" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b9b05a1 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# 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 +let id = "@p13zSAiOpguI9nsawkGijsnMfWmFd5rlUNpzekEE+vI=.ed25519"; + +let follow_ref = peach_sbotcli::follow(id)?; +let block_ref = peach_sbotcli::block(id)?; + +let invite_code = peach_sbotcli::create_invite()?; +``` + +## 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 diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..ad37b93 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,86 @@ +use core::str::Utf8Error; +use regex::Error as RegexError; +use std::{error, fmt, io::Error as IoError, string::FromUtf8Error}; + +/// 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 function which return `Result<_, SbotCliError>`. +#[derive(Debug)] +pub enum SbotCliError { + // sbotcli errors + Blob(String), + Contact(String), + GetAboutMsgs(String), + Invite(String), + Publish(String), + WhoAmI(String), + // std errors + CommandIo(IoError), + ConvertUtf8(FromUtf8Error), + InvalidUtf8(Utf8Error), + // external errors + InvalidRegex(RegexError), +} + +impl error::Error for SbotCliError {} + +impl fmt::Display for SbotCliError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + SbotCliError::Blob(ref err) => { + write!(f, "{}", err) + } + SbotCliError::Contact(ref err) => { + write!(f, "{}", err) + } + SbotCliError::GetAboutMsgs(ref err) => { + write!(f, "{}", err) + } + SbotCliError::Invite(ref err) => { + write!(f, "{}", err) + } + SbotCliError::Publish(ref err) => { + write!(f, "{}", err) + } + SbotCliError::WhoAmI(ref err) => { + write!(f, "{}", err) + } + SbotCliError::CommandIo(ref err) => { + write!(f, "{}", err) + } + SbotCliError::ConvertUtf8(ref err) => { + write!(f, "{}", err) + } + SbotCliError::InvalidUtf8(ref err) => { + write!(f, "{}", err) + } + SbotCliError::InvalidRegex(ref err) => { + write!(f, "{}", err) + } + } + } +} + +impl From for SbotCliError { + fn from(err: IoError) -> SbotCliError { + SbotCliError::CommandIo(err) + } +} + +impl From for SbotCliError { + fn from(err: FromUtf8Error) -> SbotCliError { + SbotCliError::ConvertUtf8(err) + } +} + +impl From for SbotCliError { + fn from(err: Utf8Error) -> SbotCliError { + SbotCliError::InvalidUtf8(err) + } +} + +impl From for SbotCliError { + fn from(err: RegexError) -> SbotCliError { + SbotCliError::InvalidRegex(err) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4b4bb33 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,408 @@ +//! # 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::SbotCliError; +//! +//! fn example() -> Result<(), SbotCliError> { +//! let id = "@p13zSAiOpguI9nsawkGijsnMfWmFd5rlUNpzekEE+vI=.ed25519"; +//! +//! let follow_ref = peach_sbotcli::follow(id)?; +//! let block_ref = peach_sbotcli::block(id)?; +//! +//! let invite_code = peach_sbotcli::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; + +use std::{process::Command, result::Result}; + +pub use crate::error::SbotCliError; + +/* 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(file_path: &str) -> Result { + let output = Command::new("sbotcli") + .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(id: &str) -> Result { + let output = Command::new("sbotcli") + .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(id: &str) -> Result { + let output = Command::new("sbotcli") + .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() -> Result, SbotCliError> { + let output = Command::new("sbotcli") + .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(invite: &str) -> Result { + let output = Command::new("sbotcli") + .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() -> Result { + let output = Command::new("sbotcli") + .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(blob_ref: &str, id: &str) -> Result { + let output = Command::new("sbotcli") + .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(description: &str, id: &str) -> Result { + let output = Command::new("sbotcli") + .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(id: &str, name: &str) -> Result { + let output = Command::new("sbotcli") + .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(text: &str) -> Result { + let output = Command::new("sbotcli") + .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(id: &str, msg: &str) -> Result { + let output = Command::new("sbotcli") + .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() -> Result, SbotCliError> { + let output = Command::new("sbotcli").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); + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..3e68410 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,18 @@ +use regex::Regex; + +use crate::SbotCliError; + +/// Return matches for a given Regex pattern and text. +/// +/// # Arguments +/// +/// * `pattern` - A string slice representing a regular expression +/// * `input` - A string slice representing the input to be matched on +/// +pub fn regex_finder(pattern: &str, input: &str) -> Result, SbotCliError> { + let re = Regex::new(pattern)?; + let caps = re.captures(input); + let result = caps.map(|caps| caps[1].to_string()); + + Ok(result) +}