Skip to content

Commit 12d29f0

Browse files
authored
Release/0.8.1 (#10)
* v0.8.1 - see CHANGELOG for details
1 parent ac2ec8d commit 12d29f0

File tree

10 files changed

+72
-26
lines changed

10 files changed

+72
-26
lines changed

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
## [0.8.1] - 2025-09-21
2+
3+
### Changed
4+
5+
- more resilient invocation for `create_reply()` lib fn (when `publish_media_container()` cannot be invoked as part of it)
6+
- more resilient invocation for `refresh_long_lived_bearer_token()` lib fn
7+
- dependency upgrades
8+
- updated README
9+
10+
### Fixed
11+
12+
- most API, when failed due to invalid access_token, should error out with a clearer message than the cryptic "id not found" one
13+
114
## [0.8.0] - 2025-03-31
215

316
### Changed

Cargo.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rusty_meta_threads"
3-
version = "0.8.0"
3+
version = "0.8.1"
44
edition = "2024"
55
description = "Community Rust SDK for integrating with Meta Threads API"
66
repository = "https://github.com/Thesephi/rusty_meta_threads.git"
@@ -14,10 +14,10 @@ path = "src/mod.rs"
1414
crate-type = ["rlib"]
1515

1616
[dependencies]
17-
url = "2.5.4"
17+
url = "2.5.7"
1818
urlencoding = "2.1.3"
1919
reqwest = { version = "0.12", features = ["json"] }
20-
serde = { version = "1.0.219", features = ["derive"] }
21-
log = "0.4.22"
22-
tokio = { version = "1.44.1", features = ["rt", "macros"] }
23-
env_logger = "0.11.7"
20+
serde = { version = "1.0.226", features = ["derive"] }
21+
log = "0.4.28"
22+
tokio = { version = "1.47.1", features = ["rt", "macros"] }
23+
env_logger = "0.11.8"

README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,16 @@ let refreshed_token =
7373
## Contributor notice
7474

7575
Please see [GOVERNANCE](./GOVERNANCE.md)
76+
77+
### Running tests
78+
79+
```bash
80+
export ACCESS_TOKEN=replace_with_valid_threads_api_access_token
81+
cargo test
82+
# or if more details are needed
83+
RUST_LOG=debug cargo test
84+
```
85+
Be aware: some tests may fail if `ACCESS_TOKEN` is expired, or
86+
malformed. You may get a new token as per official
87+
[Threads documentation](https://developers.facebook.com/docs/threads/get-started/get-access-tokens-and-permissions), and
88+
then using their [Postman playground](https://www.postman.com/meta/threads/request/j8jvxf4/exchange-the-code-for-a-token).

src/auth.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub struct SimpleThreadsShortLivedTokenResponse {
1717
pub struct SimpleThreadsLongLivedTokenResponse {
1818
pub access_token: Option<String>,
1919
pub token_type: Option<String>,
20-
pub expires_in: Option<u32>,
20+
pub expires_in: Option<u64>,
2121
#[allow(dead_code)]
2222
error: Option<shared::ThreadsApiRespErrorPayload>,
2323
}

src/create_reply.rs

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,22 +31,29 @@ pub async fn create_reply(
3131
}
3232
url.push_str(format!("&media_type={media_type}").as_str());
3333

34-
let media_container = reqwest::Client::new()
34+
let media_container_resp = reqwest::Client::new()
3535
.post(&url)
3636
.bearer_auth(token)
3737
.send()
38-
.await?
39-
.json::<SimpleMediaObject>()
40-
.await?;
38+
.await?; // @TODO don't silently fail on expired token (see profiles.rs example)
39+
40+
let media_container = media_container_resp.json::<SimpleMediaObject>().await?;
4141

4242
// ideally we proceed as long as we have `id` in the media_container, or poll until we have it
4343
// https://developers.facebook.com/docs/threads/troubleshooting#publishing-does-not-return-a-media-id
4444
// but for now it's alright to stick with some hardcoded wait time
4545
tokio::time::sleep(Duration::from_millis(publish_wait_time_ms)).await;
4646

47-
let res = publish_media_container(&media_container.id, token).await?;
48-
49-
Ok(res)
47+
if let Some(container_id) = media_container.id {
48+
let publish_res = publish_media_container(container_id.as_str(), token).await?;
49+
Ok(publish_res)
50+
} else {
51+
eprintln!(
52+
"could not publish reply: media_container not containing id: {:?}",
53+
media_container
54+
);
55+
Ok(media_container)
56+
}
5057
}
5158

5259
#[cfg(test)]

src/mentions.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ pub async fn get_mentions(
1717
.get(url)
1818
.bearer_auth(token)
1919
.send()
20-
.await?
20+
.await? // @TODO don't silently fail on expired token (see profiles.rs example)
2121
.json::<MetaMediaResponse<MetaMedia>>()
2222
.await?;
2323

src/oembed.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ use urlencoding::encode;
44

55
#[derive(Deserialize, Debug)]
66
pub struct OembedResponse {
7-
pub version: String,
8-
pub provider_name: String,
9-
pub provider_url: String,
10-
pub width: u64,
11-
pub html: String,
7+
pub version: Option<String>,
8+
pub provider_name: Option<String>,
9+
pub provider_url: Option<String>,
10+
pub width: Option<u64>,
11+
pub html: Option<String>,
1212
}
1313

1414
pub async fn get_oembed_html(
@@ -24,7 +24,7 @@ pub async fn get_oembed_html(
2424
.get(&url)
2525
.bearer_auth(token)
2626
.send()
27-
.await?
27+
.await? // @TODO don't silently fail on expired token (see profiles.rs example)
2828
.json::<OembedResponse>()
2929
.await?;
3030

@@ -52,6 +52,9 @@ mod tests {
5252
debug!("oembed response fetched: {:?}", res);
5353

5454
assert_eq!(true, res.is_ok());
55-
assert_eq!(res.unwrap().provider_url, "https://www.threads.net/");
55+
56+
let resp_data = res.unwrap();
57+
assert_eq!(true, resp_data.provider_url.is_some());
58+
assert_eq!(resp_data.provider_url.unwrap(), "https://www.threads.com/");
5659
}
5760
}

src/profiles.rs

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use serde::Deserialize;
44

55
#[derive(Deserialize, Debug)]
66
pub struct ThreadsUserProfile {
7-
pub id: String,
7+
pub id: Option<String>,
88
pub username: Option<String>,
99
pub name: Option<String>,
1010
pub threads_profile_picture_url: Option<String>,
@@ -36,7 +36,7 @@ pub async fn get_profile_info(
3636
debug!("failed to retrieve Threads user profile: {:#?}", error);
3737
// @TODO consider using Err instead of Ok
3838
Ok(ThreadsUserProfile {
39-
id: String::from(""),
39+
id: None,
4040
username: None,
4141
name: None,
4242
threads_biography: None,
@@ -64,6 +64,16 @@ mod tests {
6464

6565
let res = get_profile_info(None, token).await;
6666

67+
/*
68+
* @TODO test against invalid access_token, which results in this response
69+
* {
70+
* 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.",
71+
* code: 190,
72+
* error_subcode: None,
73+
* fbtrace_id: Some("A6p8XCWpTMHwh06sQG-Jv04"),
74+
* }
75+
*/
76+
6777
debug!("profile fetched {:?}", res);
6878

6979
assert_eq!(true, res.is_ok());

src/retrieve_media.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use serde::Deserialize;
33

44
#[derive(Deserialize, Debug)]
55
pub struct SimpleMediaObject {
6-
pub id: String,
6+
pub id: Option<String>,
77
}
88

99
// https://developers.facebook.com/docs/threads/reply-management#a-thread-s-conversations

src/shared.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,5 @@ pub struct ThreadsApiRespErrorPayload {
3131
#[allow(dead_code)]
3232
error_subcode: Option<u32>,
3333
#[allow(dead_code)]
34-
fbtrace_id: String,
34+
fbtrace_id: Option<String>,
3535
}

0 commit comments

Comments
 (0)