use crate::error::Result;
use crate::{AuthAction, AuthConfig, AuthUrlProvider};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_with::{
formats::{CommaSeparator, SpaceSeparator},
serde_as, StringWithSeparator,
};
pub struct AuthorizationServer {
config: AuthConfig,
}
impl AuthUrlProvider for AuthorizationServer {
type AuthRequest = AuthRequest;
type TokenRequest = GetTokenRequest;
type UserInfoRequest = GetUserInfoRequest;
fn authorize_url(request: Self::AuthRequest) -> Result<String> {
let query = serde_urlencoded::to_string(request)?;
Ok(format!(
"https://twitter.com/i/oauth2/authorize?response_type=code&{query}"
))
}
fn access_token_url(request: Self::TokenRequest) -> Result<String> {
let query = serde_urlencoded::to_string(request)?;
Ok(format!(
"https://api.x.com/2/oauth2/token?grant_type=authorization_code&{query}"
))
}
fn user_info_url(_request: Self::UserInfoRequest) -> Result<String> {
Ok(format!("https://api.x.com/2/users/me"))
}
}
#[async_trait]
impl AuthAction for AuthorizationServer {
type AuthCallback = AuthCallback;
type AuthToken = TokenResponse;
type AuthUser = UserInfoResponse;
async fn authorize<S: Into<String> + Send>(&self, state: S) -> Result<String> {
let AuthConfig {
client_id,
redirect_uri,
scope,
..
} = &self.config;
Self::authorize_url(AuthRequest {
client_id: client_id.to_string(),
redirect_uri: redirect_uri.to_string(),
state: state.into(),
scope: scope
.clone()
.or_else(|| Some(vec!["tweet.read".into(), "users.read".into()]))
.expect("scope is empty"),
..Default::default()
})
}
async fn get_access_token(&self, callback: Self::AuthCallback) -> Result<Self::AuthToken> {
let AuthConfig {
client_id,
redirect_uri,
..
} = &self.config;
let access_token_url = Self::access_token_url(GetTokenRequest {
client_id: client_id.to_string(),
code: callback.code,
redirect_uri: redirect_uri.to_string(),
code_verifier: "aaa".to_string(),
})?;
Ok(reqwest::get(access_token_url).await?.json().await?)
}
async fn get_user_info(&self, token: Self::AuthToken) -> Result<Self::AuthUser> {
let user_info_url = Self::user_info_url(GetUserInfoRequest {
user_fields: [
"created_at",
"description",
"entities",
"id",
"location",
"most_recent_tweet_id",
"name",
"pinned_tweet_id",
"profile_image_url",
"protected",
"public_metrics",
"url",
"username",
"verified",
"verified_type",
"withheld",
]
.map(|s| s.to_string())
.to_vec(),
..Default::default()
})?;
Ok(reqwest::Client::default()
.get(user_info_url)
.bearer_auth(token.access_token)
.send()
.await?
.json()
.await?)
}
}
#[serde_as]
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct AuthRequest {
client_id: String,
redirect_uri: String,
#[serde_as(as = "StringWithSeparator::<SpaceSeparator, String>")]
scope: Vec<String>,
state: String,
code_challenge: Option<String>,
code_challenge_method: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AuthCallback {
code: String,
state: String,
}
#[derive(Debug, Serialize)]
pub struct GetTokenRequest {
client_id: String,
code: String,
redirect_uri: String,
code_verifier: String,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct TokenResponse {
pub access_token: String,
pub scope: String,
pub token_type: String,
}
#[serde_as]
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct GetUserInfoRequest {
expansions: Option<String>,
#[serde(rename = "tweet.fields")]
#[serde_as(as = "StringWithSeparator::<CommaSeparator, String>")]
tweet_fields: Vec<String>,
#[serde(rename = "user.fields")]
#[serde_as(as = "StringWithSeparator::<CommaSeparator, String>")]
user_fields: Vec<String>,
}
#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct UserInfoResponse {
pub openid: String,
pub unionid: String,
pub userid: Option<u32>,
pub securemobile: Option<u32>,
pub username: Option<String>,
pub portrait: Option<String>,
pub userdetail: Option<String>,
pub birthday: Option<String>,
pub marriage: Option<String>,
pub sex: Option<String>,
pub blood: Option<String>,
pub is_bind_mobile: Option<String>,
pub is_realname: Option<String>,
}