Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
12 changes: 6 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -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"
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
2 changes: 1 addition & 1 deletion src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ pub struct SimpleThreadsShortLivedTokenResponse {
pub struct SimpleThreadsLongLivedTokenResponse {
pub access_token: Option<String>,
pub token_type: Option<String>,
pub expires_in: Option<u32>,
pub expires_in: Option<u64>,
#[allow(dead_code)]
error: Option<shared::ThreadsApiRespErrorPayload>,
}
Expand Down
21 changes: 14 additions & 7 deletions src/create_reply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<SimpleMediaObject>()
.await?;
.await?; // @TODO don't silently fail on expired token (see profiles.rs example)

let media_container = media_container_resp.json::<SimpleMediaObject>().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)]
Expand Down
2 changes: 1 addition & 1 deletion src/mentions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<MetaMediaResponse<MetaMedia>>()
.await?;

Expand Down
17 changes: 10 additions & 7 deletions src/oembed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
pub provider_name: Option<String>,
pub provider_url: Option<String>,
pub width: Option<u64>,
pub html: Option<String>,
}

pub async fn get_oembed_html(
Expand All @@ -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::<OembedResponse>()
.await?;

Expand Down Expand Up @@ -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/");
}
}
14 changes: 12 additions & 2 deletions src/profiles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use serde::Deserialize;

#[derive(Deserialize, Debug)]
pub struct ThreadsUserProfile {
pub id: String,
pub id: Option<String>,
pub username: Option<String>,
pub name: Option<String>,
pub threads_profile_picture_url: Option<String>,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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());
Expand Down
2 changes: 1 addition & 1 deletion src/retrieve_media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use serde::Deserialize;

#[derive(Deserialize, Debug)]
pub struct SimpleMediaObject {
pub id: String,
pub id: Option<String>,
}

// https://developers.facebook.com/docs/threads/reply-management#a-thread-s-conversations
Expand Down
2 changes: 1 addition & 1 deletion src/shared.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@ pub struct ThreadsApiRespErrorPayload {
#[allow(dead_code)]
error_subcode: Option<u32>,
#[allow(dead_code)]
fbtrace_id: String,
fbtrace_id: Option<String>,
}