pinterest_api/
oauth.rs

1use oauth2::{
2    basic::{
3        BasicErrorResponse, BasicRevocationErrorResponse, BasicTokenIntrospectionResponse,
4        BasicTokenType,
5    },
6    reqwest::async_http_client,
7    AuthUrl, AuthorizationCode, Client, ClientId, ClientSecret, CsrfToken, ExtraTokenFields,
8    RedirectUrl, Scope, StandardRevocableToken, StandardTokenResponse, TokenResponse, TokenUrl,
9};
10use serde::{Deserialize, Serialize};
11use std::time::Duration;
12
13use crate::error::Error;
14
15#[derive(Serialize, Deserialize, Debug)]
16#[serde(rename_all = "snake_case")]
17pub enum OAuthScope {
18    #[serde(rename = "ads:read")]
19    AdsRead,
20    #[serde(rename = "ads:write")]
21    AdsWrite,
22    #[serde(rename = "billing:read")]
23    BillingRead,
24    #[serde(rename = "billing:write")]
25    BillingWrite,
26    #[serde(rename = "biz_access:read")]
27    BizAccessRead,
28    #[serde(rename = "biz_access:write")]
29    BizAccessWrite,
30    #[serde(rename = "boards:read")]
31    BoardsRead,
32    #[serde(rename = "boards:read_secret")]
33    BoardsReadSecret,
34    #[serde(rename = "boards:write")]
35    BoardsWrite,
36    #[serde(rename = "boards:write_secret")]
37    BoardsWriteSecret,
38    #[serde(rename = "catalogs:read")]
39    CatalogsRead,
40    #[serde(rename = "catalogs:write")]
41    CatalogsWrite,
42    #[serde(rename = "pins:read")]
43    PinsRead,
44    #[serde(rename = "pins:read_secret")]
45    PinsReadSecret,
46    #[serde(rename = "pins:write")]
47    PinsWrite,
48    #[serde(rename = "pins:write_secret")]
49    PinsWriteSecret,
50    #[serde(rename = "user_accounts:read")]
51    UserAccountsRead,
52    #[serde(rename = "user_accounts:write")]
53    UserAccountsWrite,
54}
55
56impl OAuthScope {
57    pub fn all() -> Vec<Self> {
58        vec![
59            Self::AdsRead,
60            Self::AdsWrite,
61            Self::BillingRead,
62            Self::BillingWrite,
63            Self::BizAccessRead,
64            Self::BizAccessWrite,
65            Self::BoardsRead,
66            Self::BoardsReadSecret,
67            Self::BoardsWrite,
68            Self::BoardsWriteSecret,
69            Self::CatalogsRead,
70            Self::CatalogsWrite,
71            Self::PinsRead,
72            Self::PinsReadSecret,
73            Self::PinsWrite,
74            Self::PinsWriteSecret,
75            Self::UserAccountsRead,
76            Self::UserAccountsWrite,
77        ]
78    }
79}
80
81impl std::fmt::Display for OAuthScope {
82    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
83        match self {
84            Self::AdsRead => write!(f, "ads:read"),
85            Self::AdsWrite => write!(f, "ads:write"),
86            Self::BillingRead => write!(f, "billing:read"),
87            Self::BillingWrite => write!(f, "billing:write"),
88            Self::BizAccessRead => write!(f, "biz_access:read"),
89            Self::BizAccessWrite => write!(f, "biz_access:write"),
90            Self::BoardsRead => write!(f, "boards:read"),
91            Self::BoardsReadSecret => write!(f, "boards:read_secret"),
92            Self::BoardsWrite => write!(f, "boards:write"),
93            Self::BoardsWriteSecret => write!(f, "boards:write_secret"),
94            Self::CatalogsRead => write!(f, "catalogs:read"),
95            Self::CatalogsWrite => write!(f, "catalogs:write"),
96            Self::PinsRead => write!(f, "pins:read"),
97            Self::PinsReadSecret => write!(f, "pins:read_secret"),
98            Self::PinsWrite => write!(f, "pins:write"),
99            Self::PinsWriteSecret => write!(f, "pins:write_secret"),
100            Self::UserAccountsRead => write!(f, "user_accounts:read"),
101            Self::UserAccountsWrite => write!(f, "user_accounts:write"),
102        }
103    }
104}
105
106const AUTH_URL: &str = "https://www.pinterest.com/oauth/";
107const TOKEN_URL: &str = "https://api.pinterest.com/v5/oauth/token";
108
109#[derive(Debug, Clone)]
110pub struct OAuthUrlResult {
111    pub oauth_url: String,
112    pub pkce_verifier: String,
113}
114
115#[derive(Debug, Clone, Default)]
116pub struct TokenResult {
117    pub access_token: String,
118    pub refresh_token: Option<String>,
119    pub token_type: String,
120    pub extra: InnerExtraTokenFields,
121    pub expires_in: Option<Duration>,
122}
123
124#[derive(Debug, Clone, Default, Deserialize, Serialize)]
125pub struct InnerExtraTokenFields {
126    pub response_type: String,
127    pub refresh_token_expires_in: Option<u64>,
128    pub refresh_token_expires_at: Option<u64>,
129}
130impl ExtraTokenFields for InnerExtraTokenFields {}
131
132pub struct Oauth {
133    basic_client: Client<
134        BasicErrorResponse,
135        StandardTokenResponse<InnerExtraTokenFields, BasicTokenType>,
136        BasicTokenType,
137        BasicTokenIntrospectionResponse,
138        StandardRevocableToken,
139        BasicRevocationErrorResponse,
140    >,
141    redirect_url: RedirectUrl,
142    scopes: Vec<Scope>,
143}
144
145impl Oauth {
146    pub fn new(
147        api_key_code: &str,
148        api_secret_code: &str,
149        callback_url: &str,
150        scopes: Vec<OAuthScope>,
151    ) -> Result<Self, Error> {
152        let basic_client = Client::new(
153            ClientId::new(api_key_code.to_owned()),
154            Some(ClientSecret::new(api_secret_code.to_owned())),
155            AuthUrl::new(AUTH_URL.to_owned())?,
156            Some(TokenUrl::new(TOKEN_URL.to_owned())?),
157        );
158        let redirect_url = RedirectUrl::new(callback_url.to_string())?;
159        let scopes: Vec<Scope> = scopes
160            .into_iter()
161            .map(|it| Scope::new(it.to_string()))
162            .collect();
163        Ok(Self {
164            basic_client,
165            redirect_url,
166            scopes,
167        })
168    }
169
170    pub fn oauth_url(&self, state: Option<String>) -> OAuthUrlResult {
171        let (pkce_challenge, pkce_verifier) = oauth2::PkceCodeChallenge::new_random_sha256();
172        let csrf_token = match state {
173            Some(ref state_value) => CsrfToken::new(state_value.clone()),
174            None => CsrfToken::new_random(),
175        };
176        let (auth_url, _csrf_token) = self
177            .basic_client
178            .clone()
179            .set_redirect_uri(self.redirect_url.clone())
180            .authorize_url(|| csrf_token)
181            .add_scopes(self.scopes.clone())
182            .set_pkce_challenge(pkce_challenge)
183            .url();
184
185        OAuthUrlResult {
186            oauth_url: auth_url.to_string(),
187            pkce_verifier: pkce_verifier.secret().to_string(),
188        }
189    }
190
191    pub async fn token(&self, pkce_verifier_str: &str, code: &str) -> Result<TokenResult, Error> {
192        let pkce_verifier = oauth2::PkceCodeVerifier::new(pkce_verifier_str.to_owned());
193
194        let token = self
195            .basic_client
196            .clone()
197            .set_redirect_uri(self.redirect_url.clone())
198            .exchange_code(AuthorizationCode::new(code.to_owned()))
199            .set_pkce_verifier(pkce_verifier)
200            .request_async(async_http_client)
201            .await
202            .map_err(|e| Error::Oauth(format!("{:?}", e)))?;
203        Ok(TokenResult {
204            access_token: token.access_token().secret().to_string(),
205            token_type: token.token_type().as_ref().to_string(),
206            refresh_token: token.refresh_token().map(|it| it.secret().to_string()),
207            expires_in: token.expires_in(),
208            extra: token.extra_fields().clone(),
209        })
210    }
211}