ndl_core/
oauth.rs

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
25/// Exchange an authorization code for an access token
26pub 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(&params)
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}