1use serde::Deserialize;
2use thiserror::Error;
3
4pub const TOKEN_URL: &str = "https://graph.threads.net/oauth/access_token";
5pub const OAUTH_SCOPES: &str =
6 "threads_basic,threads_read_replies,threads_manage_replies,threads_content_publish";
7
8#[derive(Debug, Deserialize)]
9pub struct TokenResponse {
10 pub access_token: String,
11 #[allow(dead_code)]
12 pub user_id: u64,
13 #[serde(default)]
15 pub expires_in: Option<u64>,
16}
17
18#[derive(Debug, Error)]
19pub enum TokenExchangeError {
20 #[error("Request failed: {0}")]
21 Request(String),
22 #[error("HTTP {status}: {body}")]
23 Http { status: u16, body: String },
24 #[error("Parse error: {0}")]
25 Parse(String),
26}
27
28pub async fn exchange_code(
30 client_id: &str,
31 client_secret: &str,
32 redirect_uri: &str,
33 code: &str,
34) -> Result<TokenResponse, TokenExchangeError> {
35 let client = reqwest::Client::new();
36
37 let params = [
38 ("client_id", client_id),
39 ("client_secret", client_secret),
40 ("grant_type", "authorization_code"),
41 ("redirect_uri", redirect_uri),
42 ("code", code),
43 ];
44
45 let response = client
46 .post(TOKEN_URL)
47 .form(¶ms)
48 .send()
49 .await
50 .map_err(|e| TokenExchangeError::Request(e.to_string()))?;
51
52 if !response.status().is_success() {
53 let status = response.status().as_u16();
54 let body = response.text().await.unwrap_or_default();
55 return Err(TokenExchangeError::Http { status, body });
56 }
57
58 response
59 .json::<TokenResponse>()
60 .await
61 .map_err(|e| TokenExchangeError::Parse(e.to_string()))
62}
63
64pub async fn exchange_for_long_lived_token(
66 client_secret: &str,
67 short_lived_token: &str,
68) -> Result<TokenResponse, TokenExchangeError> {
69 let client = reqwest::Client::new();
70
71 let url = format!(
72 "https://graph.threads.net/access_token?grant_type=th_exchange_token&client_secret={}&access_token={}",
73 client_secret, short_lived_token
74 );
75
76 let response = client
77 .get(&url)
78 .send()
79 .await
80 .map_err(|e| TokenExchangeError::Request(e.to_string()))?;
81
82 if !response.status().is_success() {
83 let status = response.status().as_u16();
84 let body = response.text().await.unwrap_or_default();
85 return Err(TokenExchangeError::Http { status, body });
86 }
87
88 response
89 .json::<TokenResponse>()
90 .await
91 .map_err(|e| TokenExchangeError::Parse(e.to_string()))
92}
93
94pub async fn refresh_access_token(
96 long_lived_token: &str,
97) -> Result<TokenResponse, TokenExchangeError> {
98 let client = reqwest::Client::new();
99
100 let url = format!(
101 "https://graph.threads.net/refresh_access_token?grant_type=th_refresh_token&access_token={}",
102 long_lived_token
103 );
104
105 let response = client
106 .get(&url)
107 .send()
108 .await
109 .map_err(|e| TokenExchangeError::Request(e.to_string()))?;
110
111 if !response.status().is_success() {
112 let status = response.status().as_u16();
113 let body = response.text().await.unwrap_or_default();
114 return Err(TokenExchangeError::Http { status, body });
115 }
116
117 response
118 .json::<TokenResponse>()
119 .await
120 .map_err(|e| TokenExchangeError::Parse(e.to_string()))
121}