ytmapi_rs/auth/
oauth.rs

1use super::private::Sealed;
2use super::AuthToken;
3use crate::client::Client;
4use crate::error::{Error, Result};
5use crate::parse::ProcessedResult;
6use crate::process::RawResult;
7use crate::query::{GetQuery, PostQuery};
8use crate::{
9    query::Query,
10    utils::constants::{
11        OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, OAUTH_CODE_URL, OAUTH_GRANT_URL, OAUTH_SCOPE,
12        OAUTH_TOKEN_URL, OAUTH_USER_AGENT, USER_AGENT, YTM_API_URL, YTM_PARAMS, YTM_PARAMS_KEY,
13        YTM_URL,
14    },
15};
16use reqwest::Url;
17use serde::{Deserialize, Serialize};
18use serde_json::json;
19use std::time::{SystemTime, UNIX_EPOCH};
20
21// The original reason for the two different structs was that we did not save
22// the refresh token. But now we do, so consider simply making this only one
23// struct. Otherwise the only difference is not including Scope which is not
24// super relevant.
25#[derive(Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
26pub struct OAuthToken {
27    token_type: String,
28    access_token: String,
29    refresh_token: String,
30    expires_in: usize,
31    request_time: SystemTime,
32}
33// TODO: Lock down construction of this type.
34#[derive(Clone, Deserialize)]
35pub struct OAuthDeviceCode(String);
36
37#[derive(Clone, Deserialize)]
38struct GoogleOAuthToken {
39    pub access_token: String,
40    pub expires_in: usize,
41    pub refresh_token: String,
42    // Unused currently - for future use
43    #[allow(dead_code)]
44    pub scope: String,
45    pub token_type: String,
46}
47#[derive(Clone, Deserialize)]
48struct GoogleOAuthRefreshToken {
49    pub access_token: String,
50    pub expires_in: usize,
51    // Unused currently - for future use
52    #[allow(dead_code)]
53    pub scope: String,
54    pub token_type: String,
55}
56#[derive(Clone, Deserialize)]
57pub struct OAuthTokenGenerator {
58    pub device_code: OAuthDeviceCode,
59    pub expires_in: usize,
60    pub interval: usize,
61    pub user_code: String,
62    pub verification_url: String,
63}
64
65impl OAuthToken {
66    fn from_google_refresh_token(
67        google_token: GoogleOAuthRefreshToken,
68        request_time: SystemTime,
69        refresh_token: String,
70    ) -> Self {
71        // See comment above on OAuthToken
72        let GoogleOAuthRefreshToken {
73            access_token,
74            expires_in,
75            token_type,
76            ..
77        } = google_token;
78        Self {
79            token_type,
80            refresh_token,
81            access_token,
82            request_time,
83            expires_in,
84        }
85    }
86    fn from_google_token(google_token: GoogleOAuthToken, request_time: SystemTime) -> Self {
87        // See comment above on OAuthToken
88        let GoogleOAuthToken {
89            access_token,
90            expires_in,
91            token_type,
92            refresh_token,
93            ..
94        } = google_token;
95        Self {
96            token_type,
97            refresh_token,
98            access_token,
99            request_time,
100            expires_in,
101        }
102    }
103}
104
105impl OAuthDeviceCode {
106    pub fn new(code: String) -> Self {
107        Self(code)
108    }
109    pub fn get_code(&self) -> &str {
110        &self.0
111    }
112}
113
114impl Sealed for OAuthToken {}
115impl AuthToken for OAuthToken {
116    async fn raw_query_post<'a, Q: PostQuery + Query<Self>>(
117        &self,
118        client: &Client,
119        query: &'a Q,
120    ) -> Result<RawResult<'a, Q, OAuthToken>> {
121        // TODO: Functionize - used for Browser Auth as well.
122        let url = format!("{YTM_API_URL}{}{YTM_PARAMS}{YTM_PARAMS_KEY}", query.path());
123        let now_datetime: chrono::DateTime<chrono::Utc> = SystemTime::now().into();
124        let client_version = format!("1.{}.01.00", now_datetime.format("%Y%m%d"));
125        let mut body = json!({
126            "context" : {
127                "client" : {
128                    "clientName" : "WEB_REMIX",
129                    "clientVersion" : client_version,
130                },
131            },
132        });
133        if let Some(body) = body.as_object_mut() {
134            body.append(&mut query.header());
135        } else {
136            unreachable!("Body created in this function as an object")
137        };
138        let request_time_unix = self.request_time.duration_since(UNIX_EPOCH)?.as_secs();
139        let now_unix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
140        // TODO: Better handling for expiration case.
141        if now_unix + 3600 > request_time_unix + self.expires_in as u64 {
142            return Err(Error::oauth_token_expired(self));
143        }
144        let headers = [
145            // TODO: Confirm if parsing for expired user agent also relevant here.
146            ("User-Agent", USER_AGENT.into()),
147            ("X-Origin", YTM_URL.into()),
148            ("Content-Type", "application/json".into()),
149            (
150                "Authorization",
151                format!("{} {}", self.token_type, self.access_token).into(),
152            ),
153            ("X-Goog-Request-Time", request_time_unix.to_string().into()),
154        ];
155        let result = client
156            .post_query(url, headers, &body, &query.params())
157            .await?;
158        let result = RawResult::from_raw(result, query);
159        Ok(result)
160    }
161    async fn raw_query_get<'a, Q: GetQuery + Query<Self>>(
162        &self,
163        client: &Client,
164        query: &'a Q,
165    ) -> Result<RawResult<'a, Q, Self>> {
166        // CODE DUPLICATION WITH RAW QUERY.
167        let url = Url::parse_with_params(query.url(), query.params())
168            .map_err(|e| Error::web(format!("{e}")))?;
169        let request_time_unix = self.request_time.duration_since(UNIX_EPOCH)?.as_secs();
170        let now_unix = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs();
171        // TODO: Better handling for expiration case.
172        if now_unix + 3600 > request_time_unix + self.expires_in as u64 {
173            return Err(Error::oauth_token_expired(self));
174        }
175        let headers = [
176            // TODO: Confirm if parsing for expired user agent also relevant here.
177            ("User-Agent", USER_AGENT.into()),
178            ("X-Origin", YTM_URL.into()),
179            ("Content-Type", "application/json".into()),
180            (
181                "Authorization",
182                format!("{} {}", self.token_type, self.access_token).into(),
183            ),
184            ("X-Goog-Request-Time", request_time_unix.to_string().into()),
185        ];
186        let result = client.get_query(url, headers, &query.params()).await?;
187        let result = RawResult::from_raw(result, query);
188        Ok(result)
189    }
190    fn deserialize_json<Q: Query<Self>>(
191        raw: RawResult<Q, Self>,
192    ) -> Result<crate::parse::ProcessedResult<Q>> {
193        let processed = ProcessedResult::try_from(raw)?;
194        // Guard against error codes in json response.
195        // TODO: Add a test for this
196        if let Some(error) = processed.get_json().pointer("/error") {
197            let Some(code) = error.pointer("/code").and_then(|v| v.as_u64()) else {
198                // TODO: Better error.
199                return Err(Error::response("API reported an error but no code"));
200            };
201            let message = error
202                .pointer("/message")
203                .and_then(|s| s.as_str())
204                .map(|s| s.to_string())
205                .unwrap_or_default();
206            // TODO: Error matching
207            return Err(Error::other_code(code, message));
208        }
209        Ok(processed)
210    }
211}
212
213impl OAuthToken {
214    pub async fn from_code(client: &Client, code: OAuthDeviceCode) -> Result<OAuthToken> {
215        let body = json!({
216            "client_secret" : OAUTH_CLIENT_SECRET,
217            "grant_type" : OAUTH_GRANT_URL,
218            "code": code.get_code(),
219            "client_id" : OAUTH_CLIENT_ID
220        });
221        let headers = [("User-Agent", OAUTH_USER_AGENT.into())];
222        let result = client
223            .post_query(OAUTH_TOKEN_URL, headers, &body, &())
224            .await?;
225        let google_token: GoogleOAuthToken =
226            serde_json::from_str(&result).map_err(|_| Error::response(&result))?;
227        Ok(OAuthToken::from_google_token(
228            google_token,
229            SystemTime::now(),
230        ))
231    }
232    pub async fn refresh(&self, client: &Client) -> Result<OAuthToken> {
233        let body = json!({
234            "client_secret" : OAUTH_CLIENT_SECRET,
235            "grant_type" : "refresh_token",
236            "refresh_token" : self.refresh_token,
237            "client_id" : OAUTH_CLIENT_ID,
238        });
239        let headers = [("User-Agent", OAUTH_USER_AGENT.into())];
240        let result = client
241            .post_query(OAUTH_TOKEN_URL, headers, &body, &())
242            .await?;
243        let google_token: GoogleOAuthRefreshToken = serde_json::from_str(&result)
244            .map_err(|e| Error::unable_to_serialize_oauth(&result, e))?;
245        Ok(OAuthToken::from_google_refresh_token(
246            google_token,
247            SystemTime::now(),
248            // TODO: Remove clone.
249            self.refresh_token.clone(),
250        ))
251    }
252}
253
254impl OAuthTokenGenerator {
255    pub async fn new(client: &Client) -> Result<OAuthTokenGenerator> {
256        let body = json!({
257            "scope" : OAUTH_SCOPE,
258            "client_id" : OAUTH_CLIENT_ID
259        });
260        let headers = [("User-Agent", OAUTH_USER_AGENT.into())];
261        let result = client
262            .post_query(OAUTH_CODE_URL, headers, &body, &())
263            .await?;
264        serde_json::from_str(&result).map_err(|_| Error::response(&result))
265    }
266}
267// Don't use default Debug implementation for BrowserToken - contents are
268// private
269// TODO: Display some fields, such as time.
270impl std::fmt::Debug for OAuthToken {
271    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272        write!(f, "Private BrowserToken")
273    }
274}