diff --git a/src/error.rs b/src/error.rs index 71d503c..7060222 100644 --- a/src/error.rs +++ b/src/error.rs @@ -36,6 +36,8 @@ pub enum GolgiError { Sbot(String), /// JSON serialization or deserialization error. SerdeJson(JsonError), + /// Error decoding typed ssb message from content. + ContentType(String), /// Error decoding UTF8 string from bytes Utf8Parse { /// The underlying parse error. @@ -54,6 +56,7 @@ impl std::error::Error for GolgiError { GolgiError::Rpc(ref err) => Some(err), GolgiError::Sbot(_) => None, GolgiError::SerdeJson(ref err) => Some(err), + GolgiError::ContentType(_) => None, GolgiError::Utf8Parse{ ref source} => Some(source), } } @@ -74,6 +77,11 @@ impl std::fmt::Display for GolgiError { GolgiError::Sbot(ref err) => write!(f, "Sbot returned an error response: {}", err), GolgiError::SerdeJson(_) => write!(f, "Failed to serialize JSON slice"), //GolgiError::WhoAmI(ref err) => write!(f, "{}", err), + GolgiError::ContentType(ref err) => write!( + f, + "Failed to decode typed message from ssb message content: {}", + err + ), GolgiError::Utf8Parse{ source } => write!(f, "Failed to deserialize UTF8 from bytes: {}", source), } } diff --git a/src/messages.rs b/src/messages.rs index d422111..6ea9cc3 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -29,16 +29,49 @@ pub struct SsbMessageValue { pub signature: String, } +// Enum representing the different possible message content types +#[derive(Debug)] +pub enum SsbMessageContentType { + About, + Vote, + Post, + Contact, + Unrecognized +} + impl SsbMessageValue { - /// Gets the type field of the message content if found, - /// and if not returns "none" - pub fn get_message_type(&self) -> String { - let msg_type: String = self - .content - .get("type") - .map(|msg_type| msg_type.to_string()) - .unwrap_or_else(|| "none".to_string()); - msg_type + + /// Gets the type field of the message content as an enum, if found. + /// if no type field is found or the type field is not a string, it returns an Err(GolgiError::ContentType) + /// if a type field is found but with an unknown string it returns an Ok(SsbMessageContentType::Unrecognized) + pub fn get_message_type(&self) -> Result { + let msg_type = self + .content + .get("type") + .ok_or(GolgiError::ContentType("type field not found".to_string()))?; + let mtype_str: &str = msg_type.as_str().ok_or(GolgiError::ContentType("type field value is not a string as expected".to_string()))?; + let enum_type = match mtype_str { + "about" => SsbMessageContentType::About, + "post" => SsbMessageContentType::Post, + "vote" => SsbMessageContentType::Vote, + "contact" => SsbMessageContentType::Contact, + _ => SsbMessageContentType::Unrecognized, + }; + Ok(enum_type) + } + + /// Helper function which returns true if this message is of the given type, + /// and false if the type does not match or is not found + pub fn is_message_type(&self, message_type: SsbMessageContentType) -> bool { + let self_message_type = self.get_message_type(); + match self_message_type { + Ok(mtype) => { + matches!(mtype, message_type) + } + Err(_err) => { + false + } + } } /// Converts the content json value into an SsbMessageContent enum, diff --git a/src/sbot.rs b/src/sbot.rs index 8610410..a7b2250 100644 --- a/src/sbot.rs +++ b/src/sbot.rs @@ -5,7 +5,7 @@ use kuska_handshake::async_std::BoxStream; use kuska_sodiumoxide::crypto::{auth, sign::ed25519}; use kuska_ssb::{ api::{ - dto::{content::SubsetQuery, CreateHistoryStreamIn}, + dto::{CreateHistoryStreamIn}, ApiCaller, }, discovery, keystore, @@ -14,9 +14,12 @@ use kuska_ssb::{ }; use crate::error::GolgiError; -use crate::messages::{SsbMessageKVT, SsbMessageContent, SsbMessageValue}; +use crate::messages::{SsbMessageKVT, SsbMessageContent, SsbMessageValue, SsbMessageContentType}; use crate::utils; +// re-export types from kuska +pub use kuska_ssb::api::dto::content::SubsetQuery; + /// The Scuttlebutt identity, keys and configuration parameters for connecting to a local sbot /// instance, as well as handles for calling RPC methods and receiving responses. pub struct Sbot { @@ -146,6 +149,80 @@ impl Sbot { self.publish(msg).await } + /// Wrapper for publish which constructs and publishes an about name message appropriately from a string. + /// + /// # Arguments + /// + /// * `name` - A reference to a string slice which represents the text to be published as an about name. + pub async fn publish_name(&mut self, name: &str) -> Result { + let msg = SsbMessageContent::About { + about: self.id.to_string(), + name: Some(name.to_string()), + title: None, + branch: None, + image: None, + description: None, + location: None, + start_datetime: None, + }; + self.publish(msg).await + } + + /// Get the about messages for a particular user in order of recency. + pub async fn get_about_messages(&mut self, ssb_id: &str) -> Result, GolgiError> { + let query = SubsetQuery::Author{ + op: "author".to_string(), + feed: ssb_id.to_string(), + }; + let kvts: Vec = self.get_subset(query).await?; + let messages: Vec = kvts.into_iter().map(|kvt| kvt.value).collect(); + // TODO: after fixing sbot regression, + // change this subset query to filter by type about in addition to author + // and remove this filter section + // filter down to about messages + let mut about_messages: Vec = messages.into_iter().filter(|msg| { + msg.is_message_type(SsbMessageContentType::About) + }).collect(); + // TODO: use subset query to order messages instead of doing it this way + about_messages.sort_by(|a, b| { + b.timestamp.partial_cmp(&a.timestamp).unwrap() + }); + // return about messages + Ok(about_messages) + } + + /// Get value of latest about message with given key from given user + pub async fn get_latest_about_message(&mut self, ssb_id: &str, key: &str) -> Result, GolgiError> { + // vector of about messages with most recent at the front of the vector + let about_messages = self.get_about_messages(ssb_id).await?; + // iterate through the vector looking for most recent about message with the given key + let latest_about = about_messages + .iter() + // find the first msg that contains the field `key` + .find(|msg| msg.content.get(key).is_some()) + // map the found msg (`Some(SsbMessageValue)`) to a `Some(Some(&Value))` + .map(|msg| msg.content.get(key)) + // flatten `Some(Some(&Value))` into `Some(&Value)` + .flatten() + // map `Some(&Value)` to `Some(Some(&str))` + .map(|msg_val| msg_val.as_str()) + // flatten `Some(Some(&str))` to `Some(&str)` + .flatten() + // map `Some(&str))` to `Some(String)` + .map(|msg_str| msg_str.to_string()); + + // return value is either `Ok(Some(String))` or `Ok(None)` + Ok(latest_about) + } + + pub async fn get_name(&mut self, ssb_id: &str) -> Result, GolgiError> { + self.get_latest_about_message(ssb_id, "name").await + } + + pub async fn get_description(&mut self, ssb_id: &str) -> Result, GolgiError> { + self.get_latest_about_message(ssb_id, "description").await + } + /// Call the `createHistoryStream` RPC method and return a vector /// of SsbMessageValue. pub async fn create_history_stream( diff --git a/src/utils.rs b/src/utils.rs index c840610..34f5ec6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -140,6 +140,8 @@ where Ok(messages) } + + /// Takes in an rpc request number, and a handling function, /// and calls the handling function on all responses which match the request number, /// and prints out the result of the handling function.