diff --git a/CHANGELOG.md b/CHANGELOG.md index f78e05f..18ee7a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +## [0.8.1] - 2025-09-21 + +### Changed + +- more resilient invocation for `create_reply()` lib fn (when `publish_media_container()` cannot be invoked as part of it) +- more resilient invocation for `refresh_long_lived_bearer_token()` lib fn +- dependency upgrades +- updated README + +### Fixed + +- most API, when failed due to invalid access_token, should error out with a clearer message than the cryptic "id not found" one + ## [0.8.0] - 2025-03-31 ### Changed diff --git a/Cargo.toml b/Cargo.toml index 63d3ae8..f21a604 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "rusty_meta_threads" -version = "0.8.0" +version = "0.8.1" edition = "2024" description = "Community Rust SDK for integrating with Meta Threads API" repository = "https://github.com/Thesephi/rusty_meta_threads.git" @@ -14,10 +14,10 @@ path = "src/mod.rs" crate-type = ["rlib"] [dependencies] -url = "2.5.4" +url = "2.5.7" urlencoding = "2.1.3" reqwest = { version = "0.12", features = ["json"] } -serde = { version = "1.0.219", features = ["derive"] } -log = "0.4.22" -tokio = { version = "1.44.1", features = ["rt", "macros"] } -env_logger = "0.11.7" +serde = { version = "1.0.226", features = ["derive"] } +log = "0.4.28" +tokio = { version = "1.47.1", features = ["rt", "macros"] } +env_logger = "0.11.8" diff --git a/README.md b/README.md index 6997128..9cf42c6 100644 --- a/README.md +++ b/README.md @@ -73,3 +73,16 @@ let refreshed_token = ## Contributor notice Please see [GOVERNANCE](./GOVERNANCE.md) + +### Running tests + +```bash +export ACCESS_TOKEN=replace_with_valid_threads_api_access_token +cargo test +# or if more details are needed +RUST_LOG=debug cargo test +``` +Be aware: some tests may fail if `ACCESS_TOKEN` is expired, or +malformed. You may get a new token as per official +[Threads documentation](https://developers.facebook.com/docs/threads/get-started/get-access-tokens-and-permissions), and +then using their [Postman playground](https://www.postman.com/meta/threads/request/j8jvxf4/exchange-the-code-for-a-token). diff --git a/src/auth.rs b/src/auth.rs index 1067383..6cef437 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -17,7 +17,7 @@ pub struct SimpleThreadsShortLivedTokenResponse { pub struct SimpleThreadsLongLivedTokenResponse { pub access_token: Option, pub token_type: Option, - pub expires_in: Option, + pub expires_in: Option, #[allow(dead_code)] error: Option, } diff --git a/src/create_reply.rs b/src/create_reply.rs index 3b03657..ece5c32 100644 --- a/src/create_reply.rs +++ b/src/create_reply.rs @@ -31,22 +31,29 @@ pub async fn create_reply( } url.push_str(format!("&media_type={media_type}").as_str()); - let media_container = reqwest::Client::new() + let media_container_resp = reqwest::Client::new() .post(&url) .bearer_auth(token) .send() - .await? - .json::() - .await?; + .await?; // @TODO don't silently fail on expired token (see profiles.rs example) + + let media_container = media_container_resp.json::().await?; // ideally we proceed as long as we have `id` in the media_container, or poll until we have it // https://developers.facebook.com/docs/threads/troubleshooting#publishing-does-not-return-a-media-id // but for now it's alright to stick with some hardcoded wait time tokio::time::sleep(Duration::from_millis(publish_wait_time_ms)).await; - let res = publish_media_container(&media_container.id, token).await?; - - Ok(res) + if let Some(container_id) = media_container.id { + let publish_res = publish_media_container(container_id.as_str(), token).await?; + Ok(publish_res) + } else { + eprintln!( + "could not publish reply: media_container not containing id: {:?}", + media_container + ); + Ok(media_container) + } } #[cfg(test)] diff --git a/src/mentions.rs b/src/mentions.rs index 7236501..93646f6 100644 --- a/src/mentions.rs +++ b/src/mentions.rs @@ -17,7 +17,7 @@ pub async fn get_mentions( .get(url) .bearer_auth(token) .send() - .await? + .await? // @TODO don't silently fail on expired token (see profiles.rs example) .json::>() .await?; diff --git a/src/oembed.rs b/src/oembed.rs index 1b33134..a47684f 100644 --- a/src/oembed.rs +++ b/src/oembed.rs @@ -4,11 +4,11 @@ use urlencoding::encode; #[derive(Deserialize, Debug)] pub struct OembedResponse { - pub version: String, - pub provider_name: String, - pub provider_url: String, - pub width: u64, - pub html: String, + pub version: Option, + pub provider_name: Option, + pub provider_url: Option, + pub width: Option, + pub html: Option, } pub async fn get_oembed_html( @@ -24,7 +24,7 @@ pub async fn get_oembed_html( .get(&url) .bearer_auth(token) .send() - .await? + .await? // @TODO don't silently fail on expired token (see profiles.rs example) .json::() .await?; @@ -52,6 +52,9 @@ mod tests { debug!("oembed response fetched: {:?}", res); assert_eq!(true, res.is_ok()); - assert_eq!(res.unwrap().provider_url, "https://www.threads.net/"); + + let resp_data = res.unwrap(); + assert_eq!(true, resp_data.provider_url.is_some()); + assert_eq!(resp_data.provider_url.unwrap(), "https://www.threads.com/"); } } diff --git a/src/profiles.rs b/src/profiles.rs index 3228bf3..e090a08 100644 --- a/src/profiles.rs +++ b/src/profiles.rs @@ -4,7 +4,7 @@ use serde::Deserialize; #[derive(Deserialize, Debug)] pub struct ThreadsUserProfile { - pub id: String, + pub id: Option, pub username: Option, pub name: Option, pub threads_profile_picture_url: Option, @@ -36,7 +36,7 @@ pub async fn get_profile_info( debug!("failed to retrieve Threads user profile: {:#?}", error); // @TODO consider using Err instead of Ok Ok(ThreadsUserProfile { - id: String::from(""), + id: None, username: None, name: None, threads_biography: None, @@ -64,6 +64,16 @@ mod tests { let res = get_profile_info(None, token).await; + /* + * @TODO test against invalid access_token, which results in this response + * { + * message: "Error validating access token: The session has been invalidated because the user changed their password or Facebook has changed the session for security reasons.", + * code: 190, + * error_subcode: None, + * fbtrace_id: Some("A6p8XCWpTMHwh06sQG-Jv04"), + * } + */ + debug!("profile fetched {:?}", res); assert_eq!(true, res.is_ok()); diff --git a/src/retrieve_media.rs b/src/retrieve_media.rs index 51e273b..e6dd8af 100644 --- a/src/retrieve_media.rs +++ b/src/retrieve_media.rs @@ -3,7 +3,7 @@ use serde::Deserialize; #[derive(Deserialize, Debug)] pub struct SimpleMediaObject { - pub id: String, + pub id: Option, } // https://developers.facebook.com/docs/threads/reply-management#a-thread-s-conversations diff --git a/src/shared.rs b/src/shared.rs index f51d19c..0dc8140 100644 --- a/src/shared.rs +++ b/src/shared.rs @@ -31,5 +31,5 @@ pub struct ThreadsApiRespErrorPayload { #[allow(dead_code)] error_subcode: Option, #[allow(dead_code)] - fbtrace_id: String, + fbtrace_id: Option, }