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 #[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}