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}
14
15#[derive(Debug, Error)]
16pub enum TokenExchangeError {
17 #[error("Request failed: {0}")]
18 Request(String),
19 #[error("HTTP {status}: {body}")]
20 Http { status: u16, body: String },
21 #[error("Parse error: {0}")]
22 Parse(String),
23}
24
25pub async fn exchange_code(
27 client_id: &str,
28 client_secret: &str,
29 redirect_uri: &str,
30 code: &str,
31) -> Result<TokenResponse, TokenExchangeError> {
32 let client = reqwest::Client::new();
33
34 let params = [
35 ("client_id", client_id),
36 ("client_secret", client_secret),
37 ("grant_type", "authorization_code"),
38 ("redirect_uri", redirect_uri),
39 ("code", code),
40 ];
41
42 let response = client
43 .post(TOKEN_URL)
44 .form(¶ms)
45 .send()
46 .await
47 .map_err(|e| TokenExchangeError::Request(e.to_string()))?;
48
49 if !response.status().is_success() {
50 let status = response.status().as_u16();
51 let body = response.text().await.unwrap_or_default();
52 return Err(TokenExchangeError::Http { status, body });
53 }
54
55 response
56 .json::<TokenResponse>()
57 .await
58 .map_err(|e| TokenExchangeError::Parse(e.to_string()))
59}