From 3c779f5dbc8ac89b3dd731f1527c47540a58a352 Mon Sep 17 00:00:00 2001 From: dk Date: Sun, 21 Sep 2025 13:13:43 +0200 Subject: [PATCH 1/3] v0.8.1 - see CHANGELOG for details --- CHANGELOG.md | 11 +++++++++++ Cargo.toml | 12 ++++++------ src/auth.rs | 2 +- src/create_reply.rs | 6 ++++-- src/mentions.rs | 2 +- src/oembed.rs | 14 +++++++------- src/profiles.rs | 14 ++++++++++++-- src/retrieve_media.rs | 2 +- src/shared.rs | 2 +- 9 files changed, 44 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f78e05f..87986ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## [0.8.1] - 2025-09-21 + +### Changed + +- more resilient invocation for `refresh_long_lived_bearer_token` +- dependency upgrades + +### 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/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..c1b0ff7 100644 --- a/src/create_reply.rs +++ b/src/create_reply.rs @@ -35,7 +35,7 @@ pub async fn create_reply( .post(&url) .bearer_auth(token) .send() - .await? + .await? // @TODO don't silently fail on expired token (see profiles.rs example) .json::() .await?; @@ -44,7 +44,9 @@ pub async fn create_reply( // 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?; + assert_eq!(media_container.id.is_some(), true); + + let res = publish_media_container(media_container.id.unwrap().as_str(), token).await?; Ok(res) } 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..e7df25e 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,6 @@ mod tests { debug!("oembed response fetched: {:?}", res); assert_eq!(true, res.is_ok()); - assert_eq!(res.unwrap().provider_url, "https://www.threads.net/"); + assert_eq!(res.unwrap().provider_url.unwrap(), "https://www.threads.net/"); } } 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, } From 82b0b39b56c0e376ec39668f91f220eb14ed6d49 Mon Sep 17 00:00:00 2001 From: dk Date: Sun, 21 Sep 2025 19:29:54 +0200 Subject: [PATCH 2/3] more resilient invocation for create_reply --- CHANGELOG.md | 4 +++- README.md | 12 ++++++++++++ src/create_reply.rs | 23 ++++++++++++++--------- src/oembed.rs | 5 ++++- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87986ae..18ee7a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,10 @@ ### Changed -- more resilient invocation for `refresh_long_lived_bearer_token` +- 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 diff --git a/README.md b/README.md index 6997128..97de67e 100644 --- a/README.md +++ b/README.md @@ -73,3 +73,15 @@ 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/create_reply.rs b/src/create_reply.rs index c1b0ff7..ece5c32 100644 --- a/src/create_reply.rs +++ b/src/create_reply.rs @@ -31,24 +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? // @TODO don't silently fail on expired token (see profiles.rs example) - .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; - assert_eq!(media_container.id.is_some(), true); - - let res = publish_media_container(media_container.id.unwrap().as_str(), 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/oembed.rs b/src/oembed.rs index e7df25e..a47684f 100644 --- a/src/oembed.rs +++ b/src/oembed.rs @@ -52,6 +52,9 @@ mod tests { debug!("oembed response fetched: {:?}", res); assert_eq!(true, res.is_ok()); - assert_eq!(res.unwrap().provider_url.unwrap(), "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/"); } } From 1a89e731358d00519aff847b864ecff9242ed4af Mon Sep 17 00:00:00 2001 From: dk Date: Sun, 21 Sep 2025 20:09:14 +0200 Subject: [PATCH 3/3] updated README --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 97de67e..9cf42c6 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,7 @@ 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 +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).