diff --git a/src/error.rs b/src/error.rs index 9d93d51..ac8fab5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,6 +1,7 @@ //! Custom error type for `golgi`. use std::io::Error as IoError; +use std::str::Utf8Error; use base64::DecodeError; use kuska_handshake::async_std::Error as HandshakeError; @@ -37,6 +38,11 @@ pub enum GolgiError { SerdeJson(JsonError), /// Error decoding typed ssb message from content. ContentTypeDecode(String), + /// Error decoding UTF8 string from bytes + Utf8Parse { + /// The underlying parse error. + source: Utf8Error, + }, } impl std::error::Error for GolgiError { @@ -50,7 +56,8 @@ impl std::error::Error for GolgiError { GolgiError::Rpc(ref err) => Some(err), GolgiError::Sbot(_) => None, GolgiError::SerdeJson(ref err) => Some(err), - GolgiError::ContentTypeDecode(ref _err) => None, + GolgiError::ContentTypeDecode(_) => None, + GolgiError::Utf8Parse{ ref source} => Some(source), } } } @@ -75,6 +82,7 @@ impl std::fmt::Display for GolgiError { "Failed to decode typed message from ssb message content: {}", err ), + GolgiError::Utf8Parse{ source } => write!(f, "Failed to deserialize UTF8 from bytes: {}", source), } } } @@ -114,3 +122,9 @@ impl From for GolgiError { GolgiError::SerdeJson(err) } } + +impl From for GolgiError { + fn from(err: Utf8Error) -> Self { + GolgiError::Utf8Parse { source: err} + } +} diff --git a/src/messages.rs b/src/messages.rs index 9434e80..d839e94 100644 --- a/src/messages.rs +++ b/src/messages.rs @@ -97,7 +97,7 @@ impl SsbMessageValue { #[derive(Serialize, Deserialize, Debug)] #[serde(deny_unknown_fields)] #[allow(missing_docs)] -pub struct SsbKVT { +pub struct SsbMessageKVT { pub key: String, pub value: SsbMessageValue, pub timestamp: f64, diff --git a/src/sbot.rs b/src/sbot.rs index 56cfeda..325026d 100644 --- a/src/sbot.rs +++ b/src/sbot.rs @@ -14,7 +14,7 @@ use kuska_ssb::{ }; use crate::error::GolgiError; -use crate::messages::{SsbKVT, SsbMessageContent, SsbMessageValue, SsbMessageContentType}; +use crate::messages::{SsbMessageKVT, SsbMessageContent, SsbMessageValue, SsbMessageContentType}; use crate::utils; // re-export types from kuska @@ -91,10 +91,10 @@ impl Sbot { /// Call the `partialReplication getSubset` RPC method and return a vector /// of messages as KVTs (key, value, timestamp). // TODO: add args for `descending` and `page` (max number of msgs in response) - pub async fn get_subset(&mut self, query: SubsetQuery) -> Result, GolgiError> { + pub async fn get_subset(&mut self, query: SubsetQuery) -> Result, GolgiError> { let req_id = self.client.getsubset_req_send(query).await?; - utils::get_async_until_eof(&mut self.rpc_reader, req_id, utils::kvt_res_parse).await + utils::get_source_until_eof(&mut self.rpc_reader, req_id, utils::kvt_res_parse).await } /// Call the `whoami` RPC method and return an `id`. @@ -104,13 +104,6 @@ impl Sbot { utils::get_async(&mut self.rpc_reader, req_id, utils::string_res_parse).await } - /// Call the `createLogStream` RPC method and return a Vec - pub async fn log(&mut self) -> Result { - let req_id = self.client.log_req_send().await?; - - utils::get_async(&mut self.rpc_reader, req_id, utils::string_res_parse).await - } - /// Call the `publish` RPC method and return a message reference. /// /// # Arguments @@ -156,14 +149,32 @@ 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: String) -> Result, GolgiError> { + pub async fn get_about_messages(&mut self, ssb_id: &str) -> Result, GolgiError> { let query = SubsetQuery::Author{ op: "author".to_string(), - feed: self.id.to_string(), + feed: ssb_id.to_string(), }; - let kvts: Vec = self.get_subset(query).await?; + 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 @@ -174,14 +185,14 @@ impl Sbot { }).collect(); // TODO: use subset query to order messages instead of doing it this way about_messages.sort_by(|a, b| { - a.timestamp.partial_cmp(&b.timestamp).unwrap() + b.timestamp.partial_cmp(&a.timestamp).unwrap() }); // return about messages Ok(about_messages) } /// Get the latest description for a particular user from their about messages. - pub async fn get_description(&mut self, ssb_id: String) -> Result { + pub async fn get_description(&mut self, ssb_id: &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 an about message with a description @@ -192,7 +203,7 @@ impl Sbot { SsbMessageContent::About{description, ..} => { match description { Some(description_text) => { - return Ok(description_text) + return Ok(Some(description_text)) }, None => { continue @@ -203,7 +214,42 @@ impl Sbot { } } // if no about message with a description was found, then return the empty string - Ok("".to_string()) + Ok(None) + } + + /// 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 an about message with the given key + // the first one we find is the most recent + for msg in about_messages { + let value = msg.content.get(key); + match value { + Some(val) => { + let val_as_str_option = val.as_str(); + match val_as_str_option { + Some(val_as_str) => { + return Ok(Some(val_as_str.to_string())) + }, + None => { + // if it is an improperly formatted field (not a string) + // then just continue + continue + } + } + }, + None => { + continue + } + } + } + // if no about message with given key was found, then return None + Ok(None) + } + + pub async fn get_name(&mut self, ssb_id: &str) -> Result, GolgiError> { + self.get_latest_about_message(ssb_id, "name").await } /// Call the `createHistoryStream` RPC method and return a vector @@ -214,6 +260,6 @@ impl Sbot { ) -> Result, GolgiError> { let args = CreateHistoryStreamIn::new(id); let req_id = self.client.create_history_stream_req_send(&args).await?; - utils::get_async_until_eof(&mut self.rpc_reader, req_id, utils::ssb_message_res_parse).await + utils::get_source_until_eof(&mut self.rpc_reader, req_id, utils::ssb_message_res_parse).await } } diff --git a/src/utils.rs b/src/utils.rs index 1ab449e..c840610 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -6,16 +6,16 @@ use kuska_ssb::rpc::{RecvMsg, RequestNo, RpcReader}; use serde_json::Value; use crate::error::GolgiError; -use crate::messages::{SsbKVT, SsbMessageValue}; +use crate::messages::{SsbMessageKVT, SsbMessageValue}; /// Function to parse an array of bytes (returned by an rpc call) into a KVT. /// /// # Arguments /// /// * `body` - An array of u8 to be parsed. -pub fn kvt_res_parse(body: &[u8]) -> Result { +pub fn kvt_res_parse(body: &[u8]) -> Result { let value: Value = serde_json::from_slice(body)?; - let kvt: SsbKVT = serde_json::from_value(value)?; + let kvt: SsbMessageKVT = serde_json::from_value(value)?; Ok(kvt) } @@ -25,8 +25,7 @@ pub fn kvt_res_parse(body: &[u8]) -> Result { /// /// * `body` - An array of u8 to be parsed. pub fn string_res_parse(body: &[u8]) -> Result { - // TODO: cleanup with proper error handling etc. - Ok(std::str::from_utf8(body).unwrap().to_string()) + Ok(std::str::from_utf8(body)?.to_string()) } /// Function to parse an array of bytes (returned by an rpc call) into a serde_json::Value. @@ -104,7 +103,7 @@ where /// * `f` - A function which takes in an array of u8 and returns a Result. /// This is a function which parses the response from the RpcReader. T is a generic type, /// so this parse function can return multiple possible types (String, json, custom struct etc.) -pub async fn get_async_until_eof<'a, R, T, F>( +pub async fn get_source_until_eof<'a, R, T, F>( rpc_reader: &mut RpcReader, req_no: RequestNo, f: F,