golgi/src/api/about.rs

388 lines
14 KiB
Rust

//! Retrieve data about a peer.
//!
//! Implements the following methods:
//!
//! - [`Sbot::get_about_info`]
//! - [`Sbot::get_about_message_stream`]
//! - [`Sbot::get_description`]
//! - [`Sbot::get_latest_about_message`]
//! - [`Sbot::get_name`]
//! - [`Sbot::get_name_and_image`]
//! - [`Sbot::get_profile_info`]
use std::collections::HashMap;
use async_std::stream::{Stream, StreamExt};
use crate::{
api::get_subset::{SubsetQuery, SubsetQueryOptions},
error::GolgiError,
messages::{SsbMessageContentType, SsbMessageValue},
sbot::Sbot,
};
impl Sbot {
/// Get all the `about` type messages for a peer in order of recency
/// (ie. most recent messages first).
///
/// # Example
///
/// ```rust
/// use async_std::stream::{Stream, StreamExt};
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn about_message_stream() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
///
/// let about_message_stream = sbot_client.get_about_message_stream(ssb_id).await?;
///
/// // Make the stream into an iterator.
/// futures::pin_mut!(about_message_stream);
///
/// about_message_stream.for_each(|msg| {
/// match msg {
/// Ok(val) => println!("msg value: {:?}", val),
/// Err(e) => eprintln!("error: {}", e),
/// }
/// }).await;
///
/// Ok(())
/// }
/// ```
pub async fn get_about_message_stream(
&mut self,
ssb_id: &str,
) -> Result<impl Stream<Item = Result<SsbMessageValue, GolgiError>>, GolgiError> {
let query = SubsetQuery::Author {
op: "author".to_string(),
feed: ssb_id.to_string(),
};
// specify that most recent messages should be returned first
let query_options = SubsetQueryOptions {
descending: Some(true),
keys: None,
page_limit: None,
};
let get_subset_stream = self.get_subset_stream(query, Some(query_options)).await?;
// 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 about_message_stream = get_subset_stream.filter(|msg| match msg {
Ok(val) => val.is_message_type(SsbMessageContentType::About),
Err(_err) => false,
});
// return about message stream
Ok(about_message_stream)
}
/// Get the value of the latest `about` type message, containing the given
/// `key`, for a peer.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn name_info() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
/// let key = "name";
///
/// let name_info = sbot_client.get_latest_about_message(ssb_id, key).await?;
///
/// match name_info {
/// Some(name) => println!("peer {} is named {}", ssb_id, name),
/// None => println!("no name found for peer {}", ssb_id)
/// }
///
/// Ok(())
/// }
/// ```
pub async fn get_latest_about_message(
&mut self,
ssb_id: &str,
key: &str,
) -> Result<Option<String>, GolgiError> {
// get about_message_stream
let about_message_stream = self.get_about_message_stream(ssb_id).await?;
// now we have a stream of about messages with most recent at the front
// of the vector
futures::pin_mut!(about_message_stream);
// iterate through the vector looking for most recent about message with
// the given key
let latest_about_message_res: Option<Result<SsbMessageValue, GolgiError>> =
about_message_stream
// find the first msg that contains the field `key`
.find(|res| match res {
Ok(msg) => msg.content.get(key).is_some(),
Err(_) => false,
})
.await;
// Option<Result<SsbMessageValue, GolgiError>> -> Option<SsbMessageValue>
let latest_about_message = latest_about_message_res.and_then(|msg| msg.ok());
// Option<SsbMessageValue> -> Option<String>
let latest_about_value = latest_about_message.and_then(|msg| {
msg
// SsbMessageValue -> Option<&Value>
.content
.get(key)
// Option<&Value> -> <Option<&str>
.and_then(|value| value.as_str())
// Option<&str> -> Option<String>
.map(|value| value.to_string())
});
// return value is either `Ok(Some(String))` or `Ok(None)`
Ok(latest_about_value)
}
/// Get the latest `name`, `description` and `image` values for a peer,
/// as defined in their `about` type messages.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn profile_info() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
///
/// let profile_info = sbot_client.get_profile_info(ssb_id).await?;
///
/// let name = profile_info.get("name");
/// let description = profile_info.get("description");
/// let image = profile_info.get("image");
///
/// match (name, description, image) {
/// (Some(name), Some(desc), Some(image)) => {
/// println!(
/// "peer {} is named {}. their profile image blob reference is {} and they describe themself as follows: {}",
/// ssb_id, name, image, desc,
/// )
/// },
/// (_, _, _) => {
/// eprintln!("failed to retrieve all profile info values")
/// }
/// }
///
/// Ok(())
/// }
/// ```
pub async fn get_profile_info(
&mut self,
ssb_id: &str,
) -> Result<HashMap<String, String>, GolgiError> {
let keys_to_search_for = vec!["name", "description", "image"];
self.get_about_info(ssb_id, keys_to_search_for).await
}
/// Get the latest `name` and `image` values for a peer. This method can
/// be used to display profile images of a list of users.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn name_and_image_info() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
///
/// let profile_info = sbot_client.get_name_and_image(ssb_id).await?;
///
/// let name = profile_info.get("name");
/// let image = profile_info.get("image");
///
/// match (name, image) {
/// (Some(name), Some(image)) => {
/// println!(
/// "peer {} is named {}. their profile image blob reference is {}.",
/// ssb_id, name, image,
/// )
/// },
/// (Some(name), None) => {
/// println!(
/// "peer {} is named {}. no image blob reference was found for them.",
/// ssb_id, name,
/// )
/// },
/// (_, _) => {
/// eprintln!("failed to retrieve all profile info values")
/// }
/// }
///
/// Ok(())
/// }
pub async fn get_name_and_image(
&mut self,
ssb_id: &str,
) -> Result<HashMap<String, String>, GolgiError> {
let keys_to_search_for = vec!["name", "image"];
self.get_about_info(ssb_id, keys_to_search_for).await
}
/// Get the latest values for the provided keys from the `about` type
/// messages of a peer. The method will return once a value has been
/// found for each key, or once all messages have been checked.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn about_info() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
/// let keys_to_search_for = vec!["name", "description"];
///
/// let about_info = sbot_client.get_about_info(ssb_id, keys_to_search_for).await?;
///
/// let name = about_info.get("name");
/// let description = about_info.get("description");
///
/// match (name, description) {
/// (Some(name), Some(desc)) => {
/// println!(
/// "peer {} is named {}. they describe themself as: {}",
/// ssb_id, name, desc,
/// )
/// },
/// (Some(name), None) => {
/// println!(
/// "peer {} is named {}. no description was found for them.",
/// ssb_id, name,
/// )
/// },
/// (_, _) => {
/// eprintln!("failed to retrieve all profile info values")
/// }
/// }
///
/// Ok(())
/// }
/// ```
pub async fn get_about_info(
&mut self,
ssb_id: &str,
mut keys_to_search_for: Vec<&str>,
) -> Result<HashMap<String, String>, GolgiError> {
// get about_message_stream
let about_message_stream = self.get_about_message_stream(ssb_id).await?;
// now we have a stream of about messages with most recent at the front
// of the vector
// `pin_mut!` is needed for iteration
futures::pin_mut!(about_message_stream);
let mut profile_info: HashMap<String, String> = HashMap::new();
// iterate through the stream while it still has more values and
// we still have keys we are looking for
while let Some(res) = about_message_stream.next().await {
// if there are no more keys we are looking for, then we are done
if keys_to_search_for.is_empty() {
break;
}
// if there are still keys we are looking for, then continue searching
match res {
Ok(msg) => {
// for each key we are searching for, check if this about
// message contains a value for that key
for key in &keys_to_search_for.clone() {
let about_val = msg.content.get("about").and_then(|val| val.as_str());
let option_val = msg
.content
.get(key)
.and_then(|val| val.as_str())
.map(|val| val.to_string());
match option_val {
// only return val if this msg is about the given ssb_id
Some(val) if about_val == Some(ssb_id) => {
// if a value is found, then insert it
profile_info.insert(key.to_string(), val);
// remove this key from keys_to_search_for,
// since we are no longer searching for it
keys_to_search_for.retain(|val| val != key)
}
_ => continue,
}
}
}
Err(_err) => {
// skip errors
continue;
}
}
}
Ok(profile_info)
}
/// Get the latest `name` value for a peer.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn name_info() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
///
/// if let Some(name) = sbot_client.get_name(ssb_id).await? {
/// println!("peer {} is named {}", ssb_id, name)
/// } else {
/// eprintln!("no name found for peer {}", ssb_id)
/// }
///
/// Ok(())
/// }
/// ```
pub async fn get_name(&mut self, ssb_id: &str) -> Result<Option<String>, GolgiError> {
self.get_latest_about_message(ssb_id, "name").await
}
/// Get the latest `description` value for a peer.
///
/// # Example
///
/// ```rust
/// use golgi::{Sbot, GolgiError, sbot::Keystore};
///
/// async fn description_info() -> Result<(), GolgiError> {
/// let mut sbot_client = Sbot::init(Keystore::Patchwork, None, None).await?;
///
/// let ssb_id = "@zqshk7o2Rpd/OaZ/MxH6xXONgonP1jH+edK9+GZb/NY=.ed25519";
///
/// if let Some(desc) = sbot_client.get_description(ssb_id).await? {
/// println!("peer {} describes themself as follows: {}", ssb_id, desc)
/// } else {
/// eprintln!("no description found for peer {}", ssb_id)
/// }
///
/// Ok(())
/// }
/// ```
pub async fn get_description(&mut self, ssb_id: &str) -> Result<Option<String>, GolgiError> {
self.get_latest_about_message(ssb_id, "description").await
}
}