From c3d00837cb733a2ed205080e1b51e6f751381a98 Mon Sep 17 00:00:00 2001 From: notplants Date: Tue, 16 Nov 2021 15:41:20 +0100 Subject: [PATCH] Parse json into SsbMessageValue --- Cargo.toml | 4 + src/error.rs | 11 +++ src/lib.rs | 212 +++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 177 insertions(+), 50 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6d84409..aa5cc95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,7 @@ license = "LGPL-3.0-only" [dependencies] dirs = "4.0.0" 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" diff --git a/src/error.rs b/src/error.rs index 92ed8ea..0f45f29 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,6 +13,7 @@ pub enum SbotCliError { Blob(String), Contact(String), GetAboutMsgs(String), + GetRelationship(String), Invite(String), Publish(String), WhoAmI(String), @@ -22,6 +23,7 @@ pub enum SbotCliError { InvalidUtf8(Utf8Error), // external errors InvalidRegex(RegexError), + ParseJson(serde_json::Error), NoHomeDir, } @@ -38,6 +40,9 @@ impl fmt::Display for SbotCliError { } SbotCliError::GetAboutMsgs(ref err) => { write!(f, "{}", err) + }, + SbotCliError::GetRelationship(ref err) => { + write!(f, "{}", err) } SbotCliError::Invite(ref err) => { write!(f, "{}", err) @@ -60,6 +65,9 @@ impl fmt::Display for SbotCliError { SbotCliError::InvalidRegex(ref err) => { write!(f, "{}", err) } + SbotCliError::ParseJson(ref err) => { + write!(f, "{}", err) + } SbotCliError::NoHomeDir => { write!( f, @@ -93,3 +101,6 @@ impl From for SbotCliError { SbotCliError::InvalidRegex(err) } } + + + diff --git a/src/lib.rs b/src/lib.rs index 045a075..6f6c471 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,10 @@ //! ## License //! //! 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; mod utils; @@ -42,6 +46,43 @@ mod utils; pub use crate::error::SbotCliError; 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, + 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. pub struct Sbot { /// The path to the `sbotcli` binary. @@ -57,8 +98,8 @@ impl Sbot { /// /// # Arguments /// - /// * `sbotcli_path` - an optional string slice representing a file path - /// * `sbot_working_dir` - an optional string slice representing a directory 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 where sbotcli stores its data /// pub fn init( sbotcli_path: Option<&str>, @@ -110,7 +151,7 @@ impl Sbot { /// /// # 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 { let output = Command::new(&self.sbotcli_path) @@ -131,65 +172,154 @@ impl Sbot { /* 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. /// /// # 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 { - let output = Command::new(&self.sbotcli_path) + pub fn set_relationship(&self, id: &str, following: bool, blocking: bool) -> Result { + let mut command = Command::new(&self.sbotcli_path); + command .arg("publish") - .arg("contact") - .arg("--following") + .arg("contact"); + if following { + command.arg("--following"); + } + if blocking { + command.arg("--blocking"); + } + let output = command .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: {}", + "Error setting relationship with peer: {}", 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 { + // 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 { + 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. /// /// # 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 { - 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 - ))) - } + 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 { + 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 { + let current_relationship = self.get_relationship(id)?; + self.set_relationship(id, current_relationship.following, false) + } /* GET ABOUT MESSAGES */ @@ -551,22 +681,4 @@ mod tests { let result = 2 + 2; assert_eq!(result, 4); } - - use crate::Sbot; - - #[test] - fn new_test() { - let sbot = Sbot::init(Some("/Users/notplants/go/bin/sbotcli"), None).unwrap(); - let self_id = sbot.whoami().unwrap(); - let profile_image = "/Users/notplants/computer/docs/avi/plants.png"; - let result = sbot.add_blob(profile_image); -// sbot.publish_image(profile_image); -// let result = sbot.get_about_message("image", &self_id); -// println!("get_profile_image"); - match result { - Ok(res) => println!("success: {:?}", res), - Err(err) => println!("error: {:?}", err) - } - assert_eq!(2, 2); - } }