WIP: Change go-sbotcli-rs to parse json responses instead of using regex matches #8

Draft
notplants wants to merge 4 commits from parse-json into main
3 changed files with 237 additions and 68 deletions

View File

@ -11,3 +11,7 @@ license = "LGPL-3.0-only"
[dependencies] [dependencies]
dirs = "4.0.0" dirs = "4.0.0"
regex = "1.5.4" regex = "1.5.4"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
ssb-legacy-msg-data = "0.1.4"
ssb-multiformats = "0.4.2"

View File

@ -13,6 +13,7 @@ pub enum SbotCliError {
Blob(String), Blob(String),
Contact(String), Contact(String),
GetAboutMsgs(String), GetAboutMsgs(String),
GetRelationship(String),
Invite(String), Invite(String),
Publish(String), Publish(String),
WhoAmI(String), WhoAmI(String),
@ -22,6 +23,7 @@ pub enum SbotCliError {
InvalidUtf8(Utf8Error), InvalidUtf8(Utf8Error),
// external errors // external errors
InvalidRegex(RegexError), InvalidRegex(RegexError),
ParseJson(serde_json::Error),
NoHomeDir, NoHomeDir,
} }
@ -38,6 +40,9 @@ impl fmt::Display for SbotCliError {
} }
SbotCliError::GetAboutMsgs(ref err) => { SbotCliError::GetAboutMsgs(ref err) => {
write!(f, "{}", err) write!(f, "{}", err)
},
SbotCliError::GetRelationship(ref err) => {
write!(f, "{}", err)
} }
SbotCliError::Invite(ref err) => { SbotCliError::Invite(ref err) => {
write!(f, "{}", err) write!(f, "{}", err)
@ -60,6 +65,9 @@ impl fmt::Display for SbotCliError {
SbotCliError::InvalidRegex(ref err) => { SbotCliError::InvalidRegex(ref err) => {
write!(f, "{}", err) write!(f, "{}", err)
} }
SbotCliError::ParseJson(ref err) => {
write!(f, "{}", err)
}
SbotCliError::NoHomeDir => { SbotCliError::NoHomeDir => {
write!( write!(
f, f,
@ -93,3 +101,6 @@ impl From<RegexError> for SbotCliError {
SbotCliError::InvalidRegex(err) SbotCliError::InvalidRegex(err)
} }
} }

View File

@ -35,6 +35,10 @@
//! ## License //! ## License
//! //!
//! LGPL-3.0. //! LGPL-3.0.
use serde_json::Value;
use serde::{Serialize, Deserialize};
use ssb_multiformats::multihash::Multihash;
use ssb_legacy_msg_data::LegacyF64;
pub mod error; pub mod error;
mod utils; mod utils;
@ -42,6 +46,42 @@ mod utils;
pub use crate::error::SbotCliError; pub use crate::error::SbotCliError;
use std::{ffi::OsString, path::PathBuf, process::Command, result::Result}; use std::{ffi::OsString, path::PathBuf, process::Command, result::Result};
/// HELPER STRUCTS FOR GETTING AND SETTING VALUES FROM SBOT
/// A struct which represents any ssb message in the log.
///
/// Modified from https://github.com/sunrise-choir/ssb-validate.
///
/// Every ssb message in the log has a content field, as a first-level key.
/// This content field is further parsed based on the specific type of message.
///
/// 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).
#[derive(Serialize, Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct SsbMessageValue {
pub previous: Option<Multihash>,
pub author: String,
pub sequence: u64,
pub timestamp: LegacyF64,
pub hash: String,
pub content: Value,
pub signature: String,
}
/// A struct which represents a relationship with another user (peer).
/// Any combination of following and blocking is possible except
/// for following=true AND blocking=true.
#[derive(Serialize, Deserialize, Debug)]
pub struct PeerRelationship {
following: bool,
blocking: bool
}
/// SBOT METHODS
/// An `sbotcli` instance with associated methods for querying a Go sbot server. /// An `sbotcli` instance with associated methods for querying a Go sbot server.
pub struct Sbot { pub struct Sbot {
/// The path to the `sbotcli` binary. /// The path to the `sbotcli` binary.
@ -57,8 +97,8 @@ impl Sbot {
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `sbotcli_path` - an optional string slice representing a file path /// * `sbotcli_path` - an optional string slice representing the file path to the sbotcli binary
/// * `sbot_working_dir` - an optional string slice representing a directory path /// * `sbot_working_dir` - an optional string slice representing a directory path where sbotcli stores its data
/// ///
pub fn init( pub fn init(
sbotcli_path: Option<&str>, sbotcli_path: Option<&str>,
@ -110,7 +150,7 @@ impl Sbot {
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `file_path` - A string slice representing a file path /// * `file_path` - A string slice representing a file path to the blob to be added
/// ///
pub fn add_blob(&self, file_path: &str) -> Result<String, SbotCliError> { pub fn add_blob(&self, file_path: &str) -> Result<String, SbotCliError> {
let output = Command::new(&self.sbotcli_path) let output = Command::new(&self.sbotcli_path)
@ -131,122 +171,235 @@ impl Sbot {
/* CONTACTS */ /* CONTACTS */
/// Follow a peer. /// Set relationship with a peer.
/// ///
/// Calls `sbotcli publish contact --following [id]`. On success: trims the trailing whitespace from `stdout` /// A relationship has two properties:
/// - following (either true or false)
/// - blocking (either true or false)
///
/// This is an idempotent function which sets the relationship with a peer for following and blocking,
/// based on the arguments that are passed in.
///
/// It uses `sbotcli publish contact [id]`. Passing the --following and/or --blocking flags,
/// if following or blocking is set to true, respectively.
///
/// On success: trims the trailing whitespace from `stdout`
/// and returns the message reference. On error: returns the `stderr` output with a description. /// and returns the message reference. On error: returns the `stderr` output with a description.
/// ///
/// # Arguments /// # Arguments
/// ///
/// * `id` - A string slice representing an id (profile reference / public key) /// * `id` - A string slice representing an id (profile reference / public key) of the account to follow
/// * `following` - a boolean representing whether to follow this account or not
/// * `blocking` - a boolean representing whether to block this account or not
/// ///
pub fn follow(&self, id: &str) -> Result<String, SbotCliError> { pub fn set_relationship(&self, id: &str, following: bool, blocking: bool) -> Result<String, SbotCliError> {
let output = Command::new(&self.sbotcli_path) let mut command = Command::new(&self.sbotcli_path);
command
.arg("publish") .arg("publish")
.arg("contact") .arg("contact");
.arg("--following") if following {
command.arg("--following");
}
if blocking {
command.arg("--blocking");
}
let output = command
.arg(id) .arg(id)
.output()?; .output()?;
if output.status.success() { if output.status.success() {
let stdout = String::from_utf8(output.stdout)?; let stdout = String::from_utf8(output.stdout)?;
let msg_ref = stdout.trim_end().to_string(); let msg_ref = stdout.trim_end().to_string();
Ok(msg_ref) Ok(msg_ref)
} else { } else {
let stderr = std::str::from_utf8(&output.stderr)?; let stderr = std::str::from_utf8(&output.stderr)?;
Err(SbotCliError::Contact(format!( Err(SbotCliError::Contact(format!(
"Error following peer: {}", "Error setting relationship with peer: {}",
stderr stderr
))) )))
} }
} }
/// Block a peer. /// Get the latest value of a current relationship with a peer.
///
/// We determine the value of the relationship by looking up the most recent contact
/// message made about that peer.
///
/// If no contact message is found, then a relationship of following=false blocking=false
/// is returned.
///
/// Calls `sbotcli bytype --limit 1 --reverse contact`.
///
/// On success: parses a PeerRelationship from the ssb_message or returns one with default values.
/// On error: returns the `stderr` output with a description.
///
/// # arguments
///
/// * `id` - a string slice representing the id (profile reference / public key)
/// of the peer to look up the relationship with.
///
pub fn get_relationship(&self, _id: &str) -> Result<PeerRelationship, SbotCliError> {
// TODO: need to filter this query down to the actual user it should be for
// right now this is not a real query
let output = Command::new(&self.sbotcli_path)
.arg("bytype")
.arg("--limit")
.arg("1")
.arg("--reverse")
.arg("contact")
.output()?;
if output.status.success() {
let stdout = String::from_utf8(output.stdout)?;
println!("stdout: {:?}", stdout);
let ssb_message : SsbMessageValue = serde_json::from_str(&stdout).map_err(|err| {
SbotCliError::GetRelationship(format!("error deserializing ssb message while getting relationship to peer: {}", err))
})?;
let relationship : PeerRelationship = serde_json::from_value(ssb_message.content).map_err(|err| {
SbotCliError::GetRelationship(format!("error deserializing ssb message content while getting relationship to peer: {}", err))
})?;
Ok(relationship)
} else {
let stderr = std::str::from_utf8(&output.stderr)?;
Err(SbotCliError::GetRelationship(format!(
"error getting relationship to peer: {}",
stderr
)))
}
}
/// Follow a peer. Does not change whatever blocking relationship user already has with this peer.
///
/// 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) of the account to follow
///
pub fn follow(&self, id: &str) -> Result<String, SbotCliError> {
let current_relationship = self.get_relationship(id)?;
self.set_relationship(id, true, current_relationship.blocking)
}
/// Block a peer. Does not change whatever following relationship user already has with this 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. /// 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 /// # Arguments
/// ///
/// * `id` - A string slice representing an id (profile reference / public key) /// * `id` - A string slice representing an id (profile reference / public key) of the account to block
/// ///
pub fn block(&self, id: &str) -> Result<String, SbotCliError> { pub fn block(&self, id: &str) -> Result<String, SbotCliError> {
let current_relationship = self.get_relationship(id)?;
self.set_relationship(id, current_relationship.following, true)
}
/// Unfollow a peer. Does not change whatever blocking relationship user already has with this peer.
///
/// 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) of the account to follow
///
pub fn unfollow(&self, id: &str) -> Result<String, SbotCliError> {
let current_relationship = self.get_relationship(id)?;
self.set_relationship(id, false, current_relationship.blocking)
}
/// Unblock a peer. Does not change whatever following relationship user already has with this 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) of the account to block
///
pub fn unblock(&self, id: &str) -> Result<String, SbotCliError> {
let current_relationship = self.get_relationship(id)?;
self.set_relationship(id, current_relationship.following, false)
}
/* GET ABOUT MESSAGES */
/// Get the value of an about message with the given key.
///
/// Calls `sbotcli bytype --limit 10 --reverse about`. On success: parses the `stdout` to extract the
/// `key` and returns it. On error: returns the `stderr` output with a description.
///
/// # Arguments
///
/// * `key` - A string slice representing the name of they that is being retrieved
/// * `id` - A string slice representing the id (profile reference / public key)
/// of the user to get the about message from
///
pub fn get_about_message(&self, key: &str, _id: &str) -> Result<Option<String>, SbotCliError> {
let output = Command::new(&self.sbotcli_path) let output = Command::new(&self.sbotcli_path)
.arg("publish") .arg("bytype")
.arg("contact") .arg("--limit")
.arg("--blocking") .arg("1")
.arg(id) .arg("--reverse")
.arg("about")
.output()?; .output()?;
if output.status.success() { if output.status.success() {
// TODO: figure out how to take a stream of messages, and filter down to just ones with the value we are looking for
// this code is not real dont look at it
let stdout = String::from_utf8(output.stdout)?; let stdout = String::from_utf8(output.stdout)?;
let msg_ref = stdout.trim_end().to_string(); let ssb_message : SsbMessageValue = serde_json::from_str(&stdout).map_err(|err| {
SbotCliError::GetAboutMsgs(format!("error deserializing ssb message while getting about message: {}", err))
Ok(msg_ref) })?;
let value = ssb_message.content.get(key);
match value {
Some(val) => {
let value_as_str = val.as_str();
match value_as_str {
Some(v) => Ok(Some(v.to_string())),
None => Err(SbotCliError::GetAboutMsgs(format!("error parsing {} from about message", key)))
}
},
None => Err(SbotCliError::GetAboutMsgs(format!("error parsing {} from about message", key)))
}
} else { } else {
let stderr = std::str::from_utf8(&output.stderr)?; let stderr = std::str::from_utf8(&output.stderr)?;
Err(SbotCliError::Contact(format!( // TODO: create a more generic error variant
"Error blocking peer: {}", Err(SbotCliError::GetAboutMsgs(format!(
"Error fetching about messages: {}",
stderr stderr
))) )))
} }
} }
/* GET ABOUT MESSAGES */ /// Return latest name assignment from `about` msgs, for the public key
/// associated with the local sbot instance.
/// 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 /// 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. /// `name` and returns it. On error: returns the `stderr` output with a description.
/// ///
pub fn get_name(&self) -> Result<Option<String>, SbotCliError> { pub fn get_name(&self) -> Result<Option<String>, SbotCliError> {
let output = Command::new(&self.sbotcli_path) let self_id = self.whoami()?;
.arg("bytype") self.get_about_message("name", &self_id)
.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
)))
}
} }
/// Return latest description assignment from `about` msgs /// Return latest description assignment from `about` msgs, for the public key
/// (the description associated with the public key of the local sbot instance). /// associated with the local sbot instance.
/// ///
/// Calls `sbotcli bytype --limit 10 --reverse about`. On success: parses the `stdout` to extract the /// 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. /// `description` and returns it. On error: returns the `stderr` output with a description.
/// ///
pub fn get_description(&self) -> Result<Option<String>, SbotCliError> { pub fn get_description(&self) -> Result<Option<String>, SbotCliError> {
let output = Command::new(&self.sbotcli_path) let self_id = self.whoami()?;
.arg("bytype") self.get_about_message("description", &self_id)
.arg("--limit") }
.arg("10")
.arg("--reverse") /// Return latest profile image assignment from `about` msgs, for the public key
.arg("about") /// associated with the local sbot instance.
.output()?; ///
if output.status.success() { /// Calls `sbotcli bytype --limit 10 --reverse about`. On success: parses the `stdout` to extract the
let stdout = String::from_utf8(output.stdout)?; /// `image` and returns it. On error: returns the `stderr` output with a description.
let description = utils::regex_finder(r#""description": "(.*)""#, &stdout)?; ///
Ok(description) pub fn get_profile_image(&self) -> Result<Option<String>, SbotCliError> {
} else { let self_id = self.whoami()?;
let stderr = std::str::from_utf8(&output.stderr)?; self.get_about_message("image", &self_id)
// TODO: create a more generic error variant
Err(SbotCliError::GetAboutMsgs(format!(
"Error fetching about messages: {}",
stderr
)))
}
} }
@ -402,6 +555,7 @@ impl Sbot {
} }
/// Publish an about message with a name for one's own profile, /// Publish an about message with a name for one's own profile,
/// using whoami to find your own id. /// using whoami to find your own id.
/// ///