Skip to content

Commit 2cf8e4a

Browse files
authored
Release/0.7.0 (#7)
* v0.7.0 - see CHANGELOG for details
1 parent b5e9bf3 commit 2cf8e4a

10 files changed

+218
-5
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,5 @@ Icon
2525
Network Trash Folder
2626
Temporary Items
2727
.apdisk
28+
29+
.env

CHANGELOG.md

+8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
## [0.7.0] - 2025-03-28
2+
3+
### Added
4+
5+
- `create_reply()` lib fn
6+
- `get_oembed_html()` lib fn
7+
- `publish_media_container()` lib fn
8+
19
## [0.6.1] - 2025-03-20
210

311
### Added

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,4 @@ reqwest = { version = "0.12", features = ["json"] }
2020
serde = { version = "1.0.219", features = ["derive"] }
2121
log = "0.4.22"
2222
tokio = { version = "1.44.1", features = ["rt", "macros"] }
23+
env_logger = "0.11.7"

src/create_reply.rs

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use crate::posts::publish_media_container;
2+
use crate::retrieve_media::SimpleMediaObject;
3+
use std::time::Duration;
4+
5+
pub async fn create_reply(
6+
reply_to_id: &str,
7+
text: Option<&str>,
8+
image_url: Option<&str>,
9+
video_url: Option<&str>,
10+
token: &str,
11+
) -> Result<SimpleMediaObject, reqwest::Error> {
12+
let mut url = format!(
13+
"https://graph.threads.net/v1.0/me/threads\
14+
?reply_to_id={reply_to_id}\
15+
&access_token={token}"
16+
);
17+
18+
let mut publish_wait_time_ms = 300;
19+
let mut media_type = "TEXT";
20+
if let Some(text) = text {
21+
url.push_str(format!("&text={text}").as_str());
22+
}
23+
if let Some(image_url) = image_url {
24+
url.push_str(format!("&image_url={image_url}").as_str());
25+
media_type = "IMAGE";
26+
publish_wait_time_ms = 3000;
27+
}
28+
if let Some(video_url) = video_url {
29+
url.push_str(format!("&video_url={video_url}").as_str());
30+
media_type = "VIDEO";
31+
publish_wait_time_ms = 30000;
32+
}
33+
url.push_str(format!("&media_type={media_type}").as_str());
34+
35+
let media_container = reqwest::Client::new()
36+
.post(&url)
37+
.send()
38+
.await?
39+
.json::<SimpleMediaObject>()
40+
.await?;
41+
42+
// ideally we proceed as long as we have `id` in the media_container, or poll until we have it
43+
// https://developers.facebook.com/docs/threads/troubleshooting#publishing-does-not-return-a-media-id
44+
// but for now it's alright to stick with some hardcoded wait time
45+
tokio::time::sleep(Duration::from_millis(publish_wait_time_ms)).await;
46+
47+
let res = publish_media_container(&media_container.id, token).await?;
48+
49+
Ok(res)
50+
}
51+
52+
#[cfg(test)]
53+
mod tests {
54+
use super::*;
55+
use crate::utils::read_dot_env;
56+
use log::debug;
57+
use urlencoding::encode;
58+
59+
#[tokio::test]
60+
async fn test_create_reply() {
61+
let should_log_verbose = true;
62+
let _ = env_logger::builder()
63+
.is_test(!should_log_verbose)
64+
.try_init();
65+
66+
let env = read_dot_env();
67+
let token = env.get("ACCESS_TOKEN").unwrap();
68+
69+
let reply_to_id = "17961951074882947";
70+
let text = encode("you see me rollin' 🥁");
71+
let image_url = "https://i.imgur.com/Cj33AKk.png";
72+
let res = create_reply(reply_to_id, Some(&*text), Some(image_url), None, &token).await;
73+
74+
debug!("{:?}", res);
75+
assert_eq!(true, res.is_ok());
76+
}
77+
}

src/mentions.rs

+16-3
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,26 @@ pub async fn get_mentions(
3131
#[cfg(test)]
3232
mod tests {
3333
use super::*;
34+
use crate::utils::read_dot_env;
35+
use log::debug;
3436

3537
#[tokio::test]
36-
async fn it_works() {
37-
let res = get_mentions("foo", None, "bar").await;
38+
async fn test_get_mentions() {
39+
let should_log_verbose = true;
40+
let _ = env_logger::builder()
41+
.is_test(!should_log_verbose)
42+
.try_init();
43+
44+
let env = read_dot_env();
45+
let token = env.get("ACCESS_TOKEN").unwrap();
46+
47+
let res = get_mentions("me", None, token).await;
3848
match res {
3949
Ok(val) => match val.data {
40-
Some(dat) => assert_eq!(dat[0].id, "foo"),
50+
Some(dat) => {
51+
debug!("mentions fetched: {:?}", dat);
52+
assert_eq!(dat[0].id, "foo")
53+
}
4154
None => panic!("unexpected result"),
4255
},
4356
Err(e) => panic!("unexpected result: {}", e),

src/mod.rs

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
mod shared;
2+
pub use crate::shared::{MetaMediaResponse, Paging, ThreadsApiRespErrorPayload};
13
pub mod auth;
4+
pub mod create_reply;
25
pub mod mentions;
6+
pub mod oembed;
7+
pub mod posts;
38
pub mod profiles;
49
pub mod reply_management;
510
pub mod retrieve_media;
6-
pub use crate::shared::{MetaMediaResponse, Paging, ThreadsApiRespErrorPayload};
7-
mod shared;
811
pub mod utils;

src/oembed.rs

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
use log::debug;
2+
use serde::Deserialize;
3+
use urlencoding::encode;
4+
5+
#[derive(Deserialize, Debug)]
6+
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,
12+
}
13+
14+
pub async fn get_oembed_html(
15+
post_url: &str,
16+
token: &str,
17+
) -> Result<OembedResponse, reqwest::Error> {
18+
let the_post_url = encode(post_url);
19+
let url = format!(
20+
"https://graph.threads.net/v1.0/oembed?url={the_post_url}\
21+
&access_token={token}"
22+
);
23+
24+
debug!("requesting oembed for: {:?}", &url);
25+
26+
let res = reqwest::Client::new()
27+
.get(&url)
28+
.send()
29+
.await?
30+
.json::<OembedResponse>()
31+
.await?;
32+
33+
Ok(res)
34+
}
35+
36+
#[cfg(test)]
37+
mod tests {
38+
use super::*;
39+
use crate::utils::read_dot_env;
40+
use log::debug;
41+
42+
#[tokio::test]
43+
async fn test_get_oembed_html() {
44+
let should_log_verbose = true;
45+
let _ = env_logger::builder()
46+
.is_test(!should_log_verbose)
47+
.try_init();
48+
49+
let env = read_dot_env();
50+
let token = env.get("ACCESS_TOKEN").unwrap();
51+
52+
let res =
53+
get_oembed_html("https://www.threads.net/@dkode___/post/DHwBylVNThs", token).await;
54+
55+
debug!("oembed response fetched: {:?}", res);
56+
57+
assert_eq!(true, res.is_ok());
58+
assert_eq!(res.unwrap().html, "");
59+
}
60+
}

src/posts.rs

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use crate::retrieve_media::SimpleMediaObject;
2+
3+
pub async fn publish_media_container(
4+
media_container_id: &str,
5+
token: &str,
6+
) -> Result<SimpleMediaObject, reqwest::Error> {
7+
let url = format!(
8+
"https://graph.threads.net/v1.0/me/threads_publish\
9+
?creation_id={media_container_id}\
10+
&access_token={token}"
11+
);
12+
13+
let res = reqwest::Client::new()
14+
.post(&url)
15+
.send()
16+
.await?
17+
.json::<SimpleMediaObject>()
18+
.await?;
19+
20+
Ok(res)
21+
}

src/profiles.rs

+22
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,25 @@ pub async fn get_profile_info(bearer_token: &str) -> Result<ThreadsUserProfile,
4242
None => Ok(res),
4343
}
4444
}
45+
46+
#[cfg(test)]
47+
mod tests {
48+
use super::*;
49+
use crate::utils::read_dot_env;
50+
#[tokio::test]
51+
async fn test_get_profile_info() {
52+
let should_log_verbose = true;
53+
let _ = env_logger::builder()
54+
.is_test(!should_log_verbose)
55+
.try_init();
56+
57+
let env = read_dot_env();
58+
let token = env.get("ACCESS_TOKEN").unwrap();
59+
60+
let res = get_profile_info(token).await;
61+
62+
debug!("profile fetched {:?}", res);
63+
64+
assert_eq!(true, res.is_ok());
65+
}
66+
}

src/shared.rs

+6
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,27 @@ pub struct MetaMediaResponse<T> {
99

1010
#[derive(Deserialize, Debug)]
1111
pub struct Paging {
12+
#[allow(dead_code)]
1213
cursors: Cursors,
1314
pub next: Option<String>,
1415
}
1516

1617
#[derive(Deserialize, Debug)]
1718
pub struct Cursors {
19+
#[allow(dead_code)]
1820
before: String,
21+
#[allow(dead_code)]
1922
after: String,
2023
}
2124

2225
#[derive(Deserialize, Debug)]
2326
pub struct ThreadsApiRespErrorPayload {
2427
#[allow(dead_code)]
2528
pub message: String,
29+
#[allow(dead_code)]
2630
code: u32,
31+
#[allow(dead_code)]
2732
error_subcode: Option<u32>,
33+
#[allow(dead_code)]
2834
fbtrace_id: String,
2935
}

0 commit comments

Comments
 (0)