aoife cassidy: 1 refactor: move everything to its own file 5 files changed, 514 insertions(+), 490 deletions(-)
Copy & paste the following snippet into your terminal to import this patchset into git:
curl -s https://lists.sr.ht/~nixgoat/public-inbox/patches/51846/mbox | git am -3Learn more about email & git
also add some documentation to structs and elaborate on Threads --- src/lib.rs | 499 +------------------------------------------------ src/media.rs | 17 ++ src/post.rs | 16 ++ src/threads.rs | 458 +++++++++++++++++++++++++++++++++++++++++++++ src/user.rs | 14 ++ 5 files changed, 514 insertions(+), 490 deletions(-) create mode 100644 src/media.rs create mode 100644 src/post.rs create mode 100644 src/threads.rs create mode 100644 src/user.rs diff --git a/src/lib.rs b/src/lib.rs index 872b783..5a5c07b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,496 +19,15 @@ //! }; //! # Ok(()) //! # } +mod media; +mod post; +mod threads; +mod user; + +pub use media::{Media, MediaKind}; +pub use post::Post; +pub use threads::Threads; +pub use user::User; #[cfg(test)] mod test; - -use anyhow::Result; -use rand::distributions::{Alphanumeric, DistString}; -use reqwest::{header, Client}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use tokio::task; - -#[derive(Debug, Serialize, Deserialize)] -pub struct Post { - pub id: String, - pub name: String, - pub date: u64, - pub body: Option<String>, - pub media: Option<Vec<Media>>, - pub likes: u64, - pub reposts: u64, - pub parents: Option<Vec<String>>, - pub replies: Option<Vec<String>>, -} - -#[derive(Debug, Serialize, Deserialize)] -pub enum MediaKind { - Image, - Video, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Media { - pub kind: MediaKind, - pub alt: Option<String>, - pub content: String, - pub thumbnail: Option<String>, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct User { - pub id: u64, - pub name: Option<String>, - pub pfp: Option<String>, - pub verified: bool, - pub bio: Option<String>, - pub followers: u64, - pub links: Option<Vec<String>>, - pub posts: Option<Vec<String>>, -} - -/// Threads pseudo-client -#[derive(Debug, Clone)] -pub struct Threads { - client: Client, -} - -impl Threads { - /// Create a new [`Threads`]. - pub fn new() -> Result<Threads> { - let mut headers = header::HeaderMap::new(); - headers.insert( - "Sec-Fetch-Site", - header::HeaderValue::from_static("same-origin"), - ); - headers.insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("x-www-form-urlencoded"), - ); - - Ok(Threads { - client: Client::builder() - .default_headers(headers) - .user_agent( - "Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0", - ) - .build()?, - }) - } - - /// Send a GraphQL query to Threads and return a JSON document - #[tokio::main] - async fn query(&self, variables: &str, doc_id: &str) -> Result<Value> { - // Meta uses 11 characters, though 12 also works - let lsd = Alphanumeric.sample_string(&mut rand::thread_rng(), 11); - - let params = [ - ("lsd", lsd.as_str()), - ("variables", &format!("{{{},\"__relay_internal__pv__BarcelonaIsLoggedInrelayprovider\":false,\"__relay_internal__pv__BarcelonaIsOriginalPostPillEnabledrelayprovider\":false,\"__relay_internal__pv__BarcelonaIsThreadContextHeaderEnabledrelayprovider\":false, - \"__relay_internal__pv__BarcelonaIsSableEnabledrelayprovider\":false,\"__relay_internal__pv__BarcelonaUseCometVideoPlaybackEnginerelayprovider\":false,\"__relay_internal__pv__BarcelonaOptionalCookiesEnabledrelayprovider\":true,\"__relay_internal__pv__BarcelonaShouldShowFediverseM075Featuresrelayprovider\":false}}", variables)), - ("doc_id", doc_id), - ]; - - let resp = self - .client - .post("https://www.threads.net/api/graphql") - .form(¶ms) - .header("X-FB-LSD", lsd) - .send() - .await?; - - let deser = resp.json::<Value>().await?; - Ok(deser) - } - - /// Retrieve full post ID from short ID - #[tokio::main] - async fn full_id(&self, id: &str) -> Result<Option<String>> { - let resp = self - .client - .get(format!("https://www.threads.net/post/{}", id)) - .header("Sec-Fetch-Node", "navigate") - .send() - .await? - .text() - .await?; - - // Finds the ID, located in a meta tag containing JSON data - let id_location = resp.find("post_id"); - if id_location.is_none() { - return Ok(None); - } - - // Prepare values to select the ID - let mut cur = id_location.unwrap() + 10; - let mut curchar = resp.as_bytes()[cur] as char; - let mut id = String::new(); - - while curchar != '\"' { - id.push(curchar); - cur += 1; - curchar = resp.as_bytes()[cur] as char; - } - - Ok(Some(id)) - } - - /// Fetch user information - #[tokio::main] - pub async fn fetch_user(&self, tag: &str) -> Result<Option<User>> { - // Executes request to get user info from the username - let variables: String = format!("\"username\":\"{}\"", tag); - let cloned = self.clone(); - let resp = - task::spawn_blocking(move || cloned.query(&variables, "7394812507255098")).await??; - - // Gets tree location for value - let parent = resp - .pointer("/data/xdt_user_by_username") - .unwrap_or(&Value::Null); - - if parent.is_null() { - return Ok(None); - } - - // Defines empty values - let mut name: Option<String> = None; - let mut pfp: Option<String> = None; - let mut bio: Option<String> = None; - let mut links: Option<Vec<String>> = None; - let mut posts: Option<Vec<String>> = None; - - // These variables need to be fetched as str, otherwise they'll be wrapped in explicit quote marks - let quot = vec!["id", "full_name", "biography"]; - let mut unquot: Vec<String> = vec![]; - - for val in quot { - unquot.push(parent[val].as_str().to_owned().unwrap().to_string()) - } - - // Fetches profile picture - let pfp_location = parent - .pointer("/hd_profile_pic_versions") - .unwrap_or(&Value::Null); - - // We do this for safety, but if the request was successful, this should go smoothly. - if pfp_location.is_array() { - let pfp_versions = pfp_location.as_array().unwrap(); - - // Gets the highest quality version of the profile pic - pfp = Some( - pfp_versions[pfp_versions.len() - 1]["url"] - .as_str() - .to_owned() - .unwrap() - .to_string(), - ); - } - - // Sets name and bio values if applicable - if !unquot[1].is_empty() { - name = Some(unquot[1].clone()) - } - - if !unquot[2].is_empty() { - bio = Some(unquot[2].clone()) - } - - // Executes request to get additional information through the user ID - let cloned = self.clone(); - let id_var = format!("\"userID\":\"{}\"", unquot[0]); - let id_resp = - task::spawn_blocking(move || cloned.query(&id_var, "25253062544340717")).await??; - - // Gets user's bio links - let links_parent = id_resp - .pointer("/data/user/bio_links") - .unwrap_or(&Value::Null); - - if links_parent.is_array() { - let mut links_vec: Vec<String> = vec![]; - for x in links_parent.as_array().unwrap() { - links_vec.push(x["url"].as_str().to_owned().unwrap().to_string()) - } - links = Some(links_vec); - } - - // Executes a request to get the user's posts - let cloned = self.clone(); - let post_var = format!("\"userID\":\"{}\"", unquot[0]); - let post_resp = - task::spawn_blocking(move || cloned.query(&post_var, "7357407954367176")).await??; - - // Gets users' posts - let edges = post_resp - .pointer("/data/mediaData/edges") - .unwrap_or(&Value::Null); - if edges.is_array() { - let node_array = edges.as_array().unwrap(); - let mut post_vec: Vec<String> = vec![]; - for node in node_array { - let thread_items = node.pointer("/node/thread_items").unwrap(); - for item in thread_items.as_array().unwrap() { - let cur = item.pointer("/post").unwrap(); - let code = cur["code"].as_str().to_owned().unwrap(); - post_vec.push(code.to_string()); - } - } - posts = Some(post_vec); - } - - Ok(Some(User { - id: unquot[0].parse::<u64>()?, - name, - pfp, - bio, - links, - verified: parent["is_verified"].as_bool().unwrap_or(false), - followers: parent["follower_count"].as_u64().unwrap_or(0), - posts, - })) - } - - /// Fetch post information - #[tokio::main] - pub async fn fetch_post(&self, id: &str) -> Result<Option<Post>> { - // Since there's no endpoint for getting full IDs out of short ones, fetch it from post URL - let inner_id = id.to_owned(); - let cloned = self.clone(); - let id_req = task::spawn_blocking(move || cloned.full_id(&inner_id)).await??; - - if id_req.is_none() { - return Ok(None); - } - - let fullid = id_req.unwrap_or(String::new()); - - // Now we can fetch the actual post - let variables = format!("\"postID\":\"{}\"", &fullid); - let cloned = self.clone(); - let resp = - task::spawn_blocking(move || cloned.query(&variables, "26262423843344977")).await??; - - let check = resp.pointer("/data/data/edges"); - - if check.is_none() { - return Ok(None); - } - - // Defines values for parents and replies - let mut parents: Option<Vec<String>> = None; - let mut replies: Option<Vec<String>> = None; - - let mut parents_vec: Vec<String> = vec![]; - let mut replies_vec: Vec<String> = vec![]; - - // Defines values for post location - let mut post = &Value::Null; - let mut post_found: bool = false; - - // Meta wrapping stuff in arrays -.- - let node_array = check.unwrap_or(&Value::Null).as_array().unwrap(); - - for node in node_array { - let thread_items = node.pointer("/node/thread_items").unwrap_or(&Value::Null); - - if !thread_items.is_array() { - return Ok(None); - } - - for item in thread_items.as_array().unwrap() { - let cur = item.pointer("/post").unwrap(); - let code = cur["code"].as_str().to_owned().unwrap(); - if code == id { - post = cur; - post_found = true; - } else if !post_found { - parents_vec.push(code.to_string()); - parents = Some(parents_vec.clone()); - } else { - replies_vec.push(code.to_string()); - replies = Some(replies_vec.clone()); - } - } - } - - // Get the post's author - let tag = post - .pointer("/user/username") - .unwrap() - .as_str() - .to_owned() - .unwrap(); - - // Get the post's date - let date = post - .pointer("/taken_at") - .unwrap() - .as_u64() - .to_owned() - .unwrap(); - - // Get the post's body - let body = post - .pointer("/caption/text") - .unwrap() - .as_str() - .to_owned() - .unwrap(); - - // Locations for singular media - let video_location = post.pointer("/video_versions").unwrap_or(&Value::Null); - let image_location = post - .pointer("/image_versions2/candidates") - .unwrap_or(&Value::Null); - - // Locations for carousel media - let carousel_location = post.pointer("/carousel_media").unwrap_or(&Value::Null); - - // Define media variables - let mut media: Option<Vec<Media>> = None; - let mut media_vec: Vec<Media> = vec![]; - - // Check where media could be, if there is any - if carousel_location.is_array() { - // Carousel media - let carousel_array = carousel_location.as_array().unwrap(); - for node in carousel_array { - // Initial values - let mut kind = MediaKind::Image; - let content: String; - let mut alt: Option<String> = None; - let mut thumbnail: Option<String> = None; - - // Image - let node_image_location = &node - .pointer("/image_versions2/candidates") - .unwrap() - .as_array() - .unwrap()[0]; - let node_video_location = node.pointer("/video_versions").unwrap_or(&Value::Null); - - // CDN URL - let image_url = node_image_location["url"] - .as_str() - .to_owned() - .unwrap() - .to_string(); - - // Alt text - if !node["accessibility_caption"].is_null() { - alt = Some( - node["accessibility_caption"] - .as_str() - .to_owned() - .unwrap() - .to_string(), - ); - } - - let image = image_url.clone(); - - // Video - if node_video_location.is_array() { - let video_array = node_video_location.as_array().unwrap(); - - let video = video_array[0]["url"] - .as_str() - .to_owned() - .unwrap() - .to_string(); - - kind = MediaKind::Video; - content = video; - thumbnail = Some(image); - } else { - content = image; - } - - media_vec.push(Media { - kind, - alt, - content, - thumbnail, - }); - } - } else if image_location.is_array() - && image_location.as_array().unwrap_or(&vec![]).len() != 0 - { - // Singular media - // Initial values - let mut kind = MediaKind::Image; - let content: String; - let mut alt: Option<String> = None; - let mut thumbnail: Option<String> = None; - - // Gets the first image in URL, since it's in the highest quality - let image_array = image_location.as_array().unwrap(); - - let image_url = image_array[0]["url"] - .as_str() - .to_owned() - .unwrap() - .to_string(); - - // Alt text - if post["accessibility_caption"].is_string() { - alt = Some( - post["accessibility_caption"] - .as_str() - .to_owned() - .unwrap() - .to_string(), - ); - } - - let image = image_url.clone(); - - // Video - if video_location.is_array() { - let video_array = video_location.as_array().unwrap(); - let video = video_array[0]["url"] - .as_str() - .to_owned() - .unwrap() - .to_string(); - - kind = MediaKind::Video; - content = video; - thumbnail = Some(image); - } else { - content = image; - } - - media_vec.push(Media { - kind, - alt, - content, - thumbnail, - }) - } - - // If there was media, we add it to the response. - if media_vec.len() != 0 { - media = Some(media_vec); - } - - Ok(Some(Post { - id: fullid, - name: tag.to_string(), - date, - body: Some(body.to_string()), - media, - likes: post["like_count"].as_u64().unwrap_or(0), - reposts: post - .pointer("/text_post_app_info/repost_count") - .unwrap() - .as_u64() - .unwrap_or(0), - parents, - replies, - })) - } -} diff --git a/src/media.rs b/src/media.rs new file mode 100644 index 0000000..d75b0e2 --- /dev/null +++ b/src/media.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +/// Whether media is image or video +#[derive(Debug, Serialize, Deserialize)] +pub enum MediaKind { + Image, + Video, +} + +/// Media location and metadata +#[derive(Debug, Serialize, Deserialize)] +pub struct Media { + pub kind: MediaKind, + pub alt: Option<String>, + pub content: String, + pub thumbnail: Option<String>, +} diff --git a/src/post.rs b/src/post.rs new file mode 100644 index 0000000..50c8a4a --- /dev/null +++ b/src/post.rs @@ -0,0 +1,16 @@ +use crate::media::Media; +use serde::{Deserialize, Serialize}; + +/// Post contents, metadata, media and interactions +#[derive(Debug, Serialize, Deserialize)] +pub struct Post { + pub id: String, + pub name: String, + pub date: u64, + pub body: Option<String>, + pub media: Option<Vec<Media>>, + pub likes: u64, + pub reposts: u64, + pub parents: Option<Vec<String>>, + pub replies: Option<Vec<String>>, +} diff --git a/src/threads.rs b/src/threads.rs new file mode 100644 index 0000000..58daff0 --- /dev/null +++ b/src/threads.rs @@ -0,0 +1,458 @@ +use crate::{ + media::{Media, MediaKind}, + post::Post, + user::User, +}; +use anyhow::Result; +use rand::distributions::{Alphanumeric, DistString}; +use reqwest::{header, Client}; +use serde_json::Value; +use tokio::task; + +/// Threads pseudo-client +/// +/// All requests to the Threads API are done through [`Threads`] methods, which run the requests +/// through a [`reqwest::Client`] prefilled with the correct headers and keys Threads wants us to +/// comply with. +#[derive(Debug, Clone)] +pub struct Threads { + client: Client, +} + +impl Threads { + /// Create a new [`Threads`]. + pub fn new() -> Result<Threads> { + let mut headers = header::HeaderMap::new(); + headers.insert( + "Sec-Fetch-Site", + header::HeaderValue::from_static("same-origin"), + ); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("x-www-form-urlencoded"), + ); + + Ok(Threads { + client: Client::builder() + .default_headers(headers) + .user_agent( + "Mozilla/5.0 (X11; Linux x86_64; rv:125.0) Gecko/20100101 Firefox/125.0", + ) + .build()?, + }) + } + + /// Send a GraphQL query to Threads and return a JSON document + #[tokio::main] + async fn query(&self, variables: &str, doc_id: &str) -> Result<Value> { + // Meta uses 11 characters, though 12 also works + let lsd = Alphanumeric.sample_string(&mut rand::thread_rng(), 11); + + let params = [ + ("lsd", lsd.as_str()), + ("variables", &format!("{{{},\"__relay_internal__pv__BarcelonaIsLoggedInrelayprovider\":false,\"__relay_internal__pv__BarcelonaIsOriginalPostPillEnabledrelayprovider\":false,\"__relay_internal__pv__BarcelonaIsThreadContextHeaderEnabledrelayprovider\":false, + \"__relay_internal__pv__BarcelonaIsSableEnabledrelayprovider\":false,\"__relay_internal__pv__BarcelonaUseCometVideoPlaybackEnginerelayprovider\":false,\"__relay_internal__pv__BarcelonaOptionalCookiesEnabledrelayprovider\":true,\"__relay_internal__pv__BarcelonaShouldShowFediverseM075Featuresrelayprovider\":false}}", variables)), + ("doc_id", doc_id), + ]; + + let resp = self + .client + .post("https://www.threads.net/api/graphql") + .form(¶ms) + .header("X-FB-LSD", lsd) + .send() + .await?; + + let deser = resp.json::<Value>().await?; + Ok(deser) + } + + /// Retrieve full post ID from short ID + #[tokio::main] + async fn full_id(&self, id: &str) -> Result<Option<String>> { + let resp = self + .client + .get(format!("https://www.threads.net/post/{}", id)) + .header("Sec-Fetch-Node", "navigate") + .send() + .await? + .text() + .await?; + + // Finds the ID, located in a meta tag containing JSON data + let id_location = resp.find("post_id"); + if id_location.is_none() { + return Ok(None); + } + + // Prepare values to select the ID + let mut cur = id_location.unwrap() + 10; + let mut curchar = resp.as_bytes()[cur] as char; + let mut id = String::new(); + + while curchar != '\"' { + id.push(curchar); + cur += 1; + curchar = resp.as_bytes()[cur] as char; + } + + Ok(Some(id)) + } + + /// Fetch user information + #[tokio::main] + pub async fn fetch_user(&self, tag: &str) -> Result<Option<User>> { + // Executes request to get user info from the username + let variables: String = format!("\"username\":\"{}\"", tag); + let cloned = self.clone(); + let resp = + task::spawn_blocking(move || cloned.query(&variables, "7394812507255098")).await??; + + // Gets tree location for value + let parent = resp + .pointer("/data/xdt_user_by_username") + .unwrap_or(&Value::Null); + + if parent.is_null() { + return Ok(None); + } + + // Defines empty values + let mut name: Option<String> = None; + let mut pfp: Option<String> = None; + let mut bio: Option<String> = None; + let mut links: Option<Vec<String>> = None; + let mut posts: Option<Vec<String>> = None; + + // These variables need to be fetched as str, otherwise they'll be wrapped in explicit quote marks + let quot = vec!["id", "full_name", "biography"]; + let mut unquot: Vec<String> = vec![]; + + for val in quot { + unquot.push(parent[val].as_str().to_owned().unwrap().to_string()) + } + + // Fetches profile picture + let pfp_location = parent + .pointer("/hd_profile_pic_versions") + .unwrap_or(&Value::Null); + + // We do this for safety, but if the request was successful, this should go smoothly. + if pfp_location.is_array() { + let pfp_versions = pfp_location.as_array().unwrap(); + + // Gets the highest quality version of the profile pic + pfp = Some( + pfp_versions[pfp_versions.len() - 1]["url"] + .as_str() + .to_owned() + .unwrap() + .to_string(), + ); + } + + // Sets name and bio values if applicable + if !unquot[1].is_empty() { + name = Some(unquot[1].clone()) + } + + if !unquot[2].is_empty() { + bio = Some(unquot[2].clone()) + } + + // Executes request to get additional information through the user ID + let cloned = self.clone(); + let id_var = format!("\"userID\":\"{}\"", unquot[0]); + let id_resp = + task::spawn_blocking(move || cloned.query(&id_var, "25253062544340717")).await??; + + // Gets user's bio links + let links_parent = id_resp + .pointer("/data/user/bio_links") + .unwrap_or(&Value::Null); + + if links_parent.is_array() { + let mut links_vec: Vec<String> = vec![]; + for x in links_parent.as_array().unwrap() { + links_vec.push(x["url"].as_str().to_owned().unwrap().to_string()) + } + links = Some(links_vec); + } + + // Executes a request to get the user's posts + let cloned = self.clone(); + let post_var = format!("\"userID\":\"{}\"", unquot[0]); + let post_resp = + task::spawn_blocking(move || cloned.query(&post_var, "7357407954367176")).await??; + + // Gets users' posts + let edges = post_resp + .pointer("/data/mediaData/edges") + .unwrap_or(&Value::Null); + if edges.is_array() { + let node_array = edges.as_array().unwrap(); + let mut post_vec: Vec<String> = vec![]; + for node in node_array { + let thread_items = node.pointer("/node/thread_items").unwrap(); + for item in thread_items.as_array().unwrap() { + let cur = item.pointer("/post").unwrap(); + let code = cur["code"].as_str().to_owned().unwrap(); + post_vec.push(code.to_string()); + } + } + posts = Some(post_vec); + } + + Ok(Some(User { + id: unquot[0].parse::<u64>()?, + name, + pfp, + bio, + links, + verified: parent["is_verified"].as_bool().unwrap_or(false), + followers: parent["follower_count"].as_u64().unwrap_or(0), + posts, + })) + } + + /// Fetch post information + #[tokio::main] + pub async fn fetch_post(&self, id: &str) -> Result<Option<Post>> { + // Since there's no endpoint for getting full IDs out of short ones, fetch it from post URL + let inner_id = id.to_owned(); + let cloned = self.clone(); + let id_req = task::spawn_blocking(move || cloned.full_id(&inner_id)).await??; + + if id_req.is_none() { + return Ok(None); + } + + let fullid = id_req.unwrap_or(String::new()); + + // Now we can fetch the actual post + let variables = format!("\"postID\":\"{}\"", &fullid); + let cloned = self.clone(); + let resp = + task::spawn_blocking(move || cloned.query(&variables, "26262423843344977")).await??; + + let check = resp.pointer("/data/data/edges"); + + if check.is_none() { + return Ok(None); + } + + // Defines values for parents and replies + let mut parents: Option<Vec<String>> = None; + let mut replies: Option<Vec<String>> = None; + + let mut parents_vec: Vec<String> = vec![]; + let mut replies_vec: Vec<String> = vec![]; + + // Defines values for post location + let mut post = &Value::Null; + let mut post_found: bool = false; + + // Meta wrapping stuff in arrays -.- + let node_array = check.unwrap_or(&Value::Null).as_array().unwrap(); + + for node in node_array { + let thread_items = node.pointer("/node/thread_items").unwrap_or(&Value::Null); + + if !thread_items.is_array() { + return Ok(None); + } + + for item in thread_items.as_array().unwrap() { + let cur = item.pointer("/post").unwrap(); + let code = cur["code"].as_str().to_owned().unwrap(); + if code == id { + post = cur; + post_found = true; + } else if !post_found { + parents_vec.push(code.to_string()); + parents = Some(parents_vec.clone()); + } else { + replies_vec.push(code.to_string()); + replies = Some(replies_vec.clone()); + } + } + } + + // Get the post's author + let tag = post + .pointer("/user/username") + .unwrap() + .as_str() + .to_owned() + .unwrap(); + + // Get the post's date + let date = post + .pointer("/taken_at") + .unwrap() + .as_u64() + .to_owned() + .unwrap(); + + // Get the post's body + let body = post + .pointer("/caption/text") + .unwrap() + .as_str() + .to_owned() + .unwrap(); + + // Locations for singular media + let video_location = post.pointer("/video_versions").unwrap_or(&Value::Null); + let image_location = post + .pointer("/image_versions2/candidates") + .unwrap_or(&Value::Null); + + // Locations for carousel media + let carousel_location = post.pointer("/carousel_media").unwrap_or(&Value::Null); + + // Define media variables + let mut media: Option<Vec<Media>> = None; + let mut media_vec: Vec<Media> = vec![]; + + // Check where media could be, if there is any + if carousel_location.is_array() { + // Carousel media + let carousel_array = carousel_location.as_array().unwrap(); + for node in carousel_array { + // Initial values + let mut kind = MediaKind::Image; + let content: String; + let mut alt: Option<String> = None; + let mut thumbnail: Option<String> = None; + + // Image + let node_image_location = &node + .pointer("/image_versions2/candidates") + .unwrap() + .as_array() + .unwrap()[0]; + let node_video_location = node.pointer("/video_versions").unwrap_or(&Value::Null); + + // CDN URL + let image_url = node_image_location["url"] + .as_str() + .to_owned() + .unwrap() + .to_string(); + + // Alt text + if !node["accessibility_caption"].is_null() { + alt = Some( + node["accessibility_caption"] + .as_str() + .to_owned() + .unwrap() + .to_string(), + ); + } + + let image = image_url.clone(); + + // Video + if node_video_location.is_array() { + let video_array = node_video_location.as_array().unwrap(); + + let video = video_array[0]["url"] + .as_str() + .to_owned() + .unwrap() + .to_string(); + + kind = MediaKind::Video; + content = video; + thumbnail = Some(image); + } else { + content = image; + } + + media_vec.push(Media { + kind, + alt, + content, + thumbnail, + }); + } + } else if image_location.is_array() + && image_location.as_array().unwrap_or(&vec![]).len() != 0 + { + // Singular media + // Initial values + let mut kind = MediaKind::Image; + let content: String; + let mut alt: Option<String> = None; + let mut thumbnail: Option<String> = None; + + // Gets the first image in URL, since it's in the highest quality + let image_array = image_location.as_array().unwrap(); + + let image_url = image_array[0]["url"] + .as_str() + .to_owned() + .unwrap() + .to_string(); + + // Alt text + if post["accessibility_caption"].is_string() { + alt = Some( + post["accessibility_caption"] + .as_str() + .to_owned() + .unwrap() + .to_string(), + ); + } + + let image = image_url.clone(); + + // Video + if video_location.is_array() { + let video_array = video_location.as_array().unwrap(); + let video = video_array[0]["url"] + .as_str() + .to_owned() + .unwrap() + .to_string(); + + kind = MediaKind::Video; + content = video; + thumbnail = Some(image); + } else { + content = image; + } + + media_vec.push(Media { + kind, + alt, + content, + thumbnail, + }) + } + + // If there was media, we add it to the response. + if media_vec.len() != 0 { + media = Some(media_vec); + } + + Ok(Some(Post { + id: fullid, + name: tag.to_string(), + date, + body: Some(body.to_string()), + media, + likes: post["like_count"].as_u64().unwrap_or(0), + reposts: post + .pointer("/text_post_app_info/repost_count") + .unwrap() + .as_u64() + .unwrap_or(0), + parents, + replies, + })) + } +} diff --git a/src/user.rs b/src/user.rs new file mode 100644 index 0000000..d561792 --- /dev/null +++ b/src/user.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +/// User information and statistics +#[derive(Debug, Serialize, Deserialize)] +pub struct User { + pub id: u64, + pub name: Option<String>, + pub pfp: Option<String>, + pub verified: bool, + pub bio: Option<String>, + pub followers: u64, + pub links: Option<Vec<String>>, + pub posts: Option<Vec<String>>, +} -- 2.44.0
builds.sr.ht <builds@sr.ht>spools/patches/.build.yml: SUCCESS in 1m29s [refactor: move everything to its own file][0] from [aoife cassidy][1] [0]: https://lists.sr.ht/~nixgoat/public-inbox/patches/51846 [1]: mailto:aoife@enby.space ✓ #1220589 SUCCESS spools/patches/.build.yml https://builds.sr.ht/~nixgoat/job/1220589