Skip to main content

twapi_oauth2/
x.rs

1use std::time::Duration;
2
3use reqwest::{StatusCode, header::HeaderMap};
4
5use crate::{
6    CodeChallengeMethod, PkceS256, ResponseType, TokenResult, authorize_url, error::OAuth2Error,
7};
8
9pub enum XScope {
10    TweetRead,
11    TweetWrite,
12    TweetModerateWrite,
13    UsersEmail,
14    UsersRead,
15    FollowsRead,
16    FollowsWrite,
17    OfflineAccess,
18    SpaceRead,
19    MuteRead,
20    MuteWrite,
21    LikeRead,
22    LikeWrite,
23    ListRead,
24    ListWrite,
25    BlockRead,
26    BlockWrite,
27    BookmarkRead,
28    BookmarkWrite,
29    DmRead,
30    DmWrite,
31    MediaWrite,
32}
33
34impl XScope {
35    pub fn all() -> Vec<Self> {
36        vec![
37            Self::TweetRead,
38            Self::TweetWrite,
39            Self::TweetModerateWrite,
40            Self::UsersEmail,
41            Self::UsersRead,
42            Self::FollowsRead,
43            Self::FollowsWrite,
44            Self::OfflineAccess,
45            Self::SpaceRead,
46            Self::MuteRead,
47            Self::MuteWrite,
48            Self::LikeRead,
49            Self::LikeWrite,
50            Self::ListRead,
51            Self::ListWrite,
52            Self::BlockRead,
53            Self::BlockWrite,
54            Self::BookmarkRead,
55            Self::BookmarkWrite,
56            Self::DmRead,
57            Self::DmWrite,
58            Self::MediaWrite,
59        ]
60    }
61
62    pub fn scopes_to_string(scopes: &[XScope]) -> String {
63        scopes
64            .iter()
65            .map(|s| s.to_string())
66            .collect::<Vec<String>>()
67            .join(" ")
68    }
69}
70
71impl std::fmt::Display for XScope {
72    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
73        match self {
74            Self::TweetRead => write!(f, "tweet.read"),
75            Self::TweetWrite => write!(f, "tweet.write"),
76            Self::TweetModerateWrite => write!(f, "tweet.moderate.write"),
77            Self::UsersEmail => write!(f, "users.email"),
78            Self::UsersRead => write!(f, "users.read"),
79            Self::FollowsRead => write!(f, "follows.read"),
80            Self::FollowsWrite => write!(f, "follows.write"),
81            Self::OfflineAccess => write!(f, "offline.access"),
82            Self::SpaceRead => write!(f, "space.read"),
83            Self::MuteRead => write!(f, "mute.read"),
84            Self::MuteWrite => write!(f, "mute.write"),
85            Self::LikeRead => write!(f, "like.read"),
86            Self::LikeWrite => write!(f, "like.write"),
87            Self::ListRead => write!(f, "list.read"),
88            Self::ListWrite => write!(f, "list.write"),
89            Self::BlockRead => write!(f, "block.read"),
90            Self::BlockWrite => write!(f, "block.write"),
91            Self::BookmarkRead => write!(f, "bookmark.read"),
92            Self::BookmarkWrite => write!(f, "bookmark.write"),
93            Self::DmRead => write!(f, "dm.read"),
94            Self::DmWrite => write!(f, "dm.write"),
95            Self::MediaWrite => write!(f, "media.write"),
96        }
97    }
98}
99
100pub const X_AUTHORIZE_URL: &str = "https://x.com/i/oauth2/authorize";
101pub const X_TOKEN_URL: &str = "https://api.x.com/2/oauth2/token";
102
103pub struct XClient {
104    client_id: String,
105    client_secret: String,
106    redirect_uri: String,
107    scopes: Vec<XScope>,
108    try_count: usize,
109    retry_millis: u64,
110    timeout: Duration,
111}
112
113impl XClient {
114    pub fn new(
115        client_id: &str,
116        client_secret: &str,
117        redirect_uri: &str,
118        scopes: Vec<XScope>,
119    ) -> Self {
120        Self::new_with_token_options(
121            client_id,
122            client_secret,
123            redirect_uri,
124            scopes,
125            3,
126            500,
127            Duration::from_secs(10),
128        )
129    }
130
131    pub fn new_with_token_options(
132        client_id: &str,
133        client_secret: &str,
134        redirect_uri: &str,
135        scopes: Vec<XScope>,
136        try_count: usize,
137        retry_millis: u64,
138        timeout: Duration,
139    ) -> Self {
140        Self {
141            client_id: client_id.to_string(),
142            client_secret: client_secret.to_string(),
143            redirect_uri: redirect_uri.to_string(),
144            scopes,
145            try_count,
146            retry_millis,
147            timeout,
148        }
149    }
150
151    pub fn authorize_url(&self, state: &str) -> (String, String) {
152        let pkce = PkceS256::new();
153
154        let scopes_str = XScope::scopes_to_string(&self.scopes);
155        (
156            authorize_url(
157                X_AUTHORIZE_URL,
158                ResponseType::Code,
159                &self.client_id,
160                &self.redirect_uri,
161                &scopes_str,
162                state,
163                &pkce.code_challenge,
164                CodeChallengeMethod::S256,
165            ),
166            pkce.code_verifier,
167        )
168    }
169
170    pub async fn token(
171        &self,
172        code: &str,
173        code_verifier: &str,
174    ) -> Result<(TokenResult, StatusCode, HeaderMap), OAuth2Error> {
175        let (token_json, status_code, headers) = crate::token(
176            X_TOKEN_URL,
177            &self.client_id,
178            &self.client_secret,
179            &self.redirect_uri,
180            code,
181            code_verifier,
182            "authorization_code",
183            self.timeout,
184            self.try_count,
185            self.retry_millis,
186        )
187        .await?;
188        Ok((token_json, status_code, headers))
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use crate::x::XScope;
195
196    use super::*;
197
198    // CLIENT_ID=xxx CLIENT_SECRET=xxx REDIRECT_URL=http://localhost:8000/callback cargo test test_x_authorize -- --nocapture
199    #[tokio::test]
200    async fn test_x_authorize() {
201        let client_id = std::env::var("CLIENT_ID").unwrap();
202        let client_secret = std::env::var("CLIENT_SECRET").unwrap();
203        let redirect_url = std::env::var("REDIRECT_URL").unwrap();
204        let state = "test_state";
205        let x_client = XClient::new(&client_id, &client_secret, &redirect_url, XScope::all());
206        let (auth_url, code_verifier) = x_client.authorize_url(state);
207        println!("Authorize URL: {}", auth_url);
208        println!("Code Verifier: {}", code_verifier);
209    }
210}