1use twitch_types::{UserIdRef, UserNameRef};
2
3#[cfg(not(all(target_family = "wasm", target_os = "unknown")))]
4use std::time::Instant;
5
6#[cfg(all(target_family = "wasm", target_os = "unknown"))]
7use web_time::Instant;
8
9#[cfg(feature = "client")]
10use super::errors::{AppAccessTokenError, ValidationError};
11#[cfg(feature = "client")]
12use crate::client::Client;
13#[cfg(feature = "client")]
14use crate::tokens::errors::RefreshTokenError;
15use crate::tokens::{Scope, TwitchToken};
16use crate::{
17 types::{AccessToken, ClientId, ClientSecret, RefreshToken},
18 ClientIdRef, ClientSecretRef,
19};
20
21#[derive(Clone)]
28pub struct AppAccessToken {
29 pub access_token: AccessToken,
31 pub refresh_token: Option<RefreshToken>,
33 expires_in: std::time::Duration,
35 struct_created: Instant,
37 client_id: ClientId,
38 client_secret: ClientSecret,
39 scopes: Vec<Scope>,
40}
41
42impl std::fmt::Debug for AppAccessToken {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.debug_struct("AppAccessToken")
45 .field("access_token", &self.access_token)
46 .field("refresh_token", &self.refresh_token)
47 .field("client_id", &self.client_id)
48 .field("client_secret", &self.client_secret)
49 .field("expires_in", &self.expires_in())
50 .field("scopes", &self.scopes)
51 .finish()
52 }
53}
54
55impl TwitchToken for AppAccessToken {
56 fn token_type() -> super::BearerTokenType { super::BearerTokenType::AppAccessToken }
57
58 fn client_id(&self) -> &ClientId { &self.client_id }
59
60 fn token(&self) -> &AccessToken { &self.access_token }
61
62 fn login(&self) -> Option<&UserNameRef> { None }
63
64 fn user_id(&self) -> Option<&UserIdRef> { None }
65
66 #[cfg(feature = "client")]
67 async fn refresh_token<'a, C>(
68 &mut self,
69 http_client: &'a C,
70 ) -> Result<(), RefreshTokenError<<C as Client>::Error>>
71 where
72 C: Client,
73 {
74 let (access_token, expires_in, refresh_token) =
75 if let Some(token) = self.refresh_token.take() {
76 token
77 .refresh_token(http_client, &self.client_id, Some(&self.client_secret))
78 .await?
79 } else {
80 return Err(RefreshTokenError::NoRefreshToken);
81 };
82 self.access_token = access_token;
83 self.expires_in = expires_in;
84 self.refresh_token = refresh_token;
85 self.struct_created = Instant::now();
86 Ok(())
87 }
88
89 fn expires_in(&self) -> std::time::Duration {
90 self.expires_in
91 .checked_sub(self.struct_created.elapsed())
92 .unwrap_or_default()
93 }
94
95 fn scopes(&self) -> &[Scope] { self.scopes.as_slice() }
96}
97
98impl AppAccessToken {
99 pub fn from_existing_unchecked(
108 access_token: AccessToken,
109 refresh_token: impl Into<Option<RefreshToken>>,
110 client_id: impl Into<ClientId>,
111 client_secret: impl Into<ClientSecret>,
112 scopes: Option<Vec<Scope>>,
113 expires_in: Option<std::time::Duration>,
114 ) -> AppAccessToken {
115 AppAccessToken {
116 access_token,
117 refresh_token: refresh_token.into(),
118 client_id: client_id.into(),
119 client_secret: client_secret.into(),
120 expires_in: expires_in.unwrap_or_default(),
121 struct_created: Instant::now(),
122 scopes: scopes.unwrap_or_default(),
123 }
124 }
125
126 #[cfg(feature = "client")]
128 pub async fn from_existing<C>(
129 http_client: &C,
130 access_token: AccessToken,
131 refresh_token: impl Into<Option<RefreshToken>>,
132 client_secret: ClientSecret,
133 ) -> Result<AppAccessToken, ValidationError<<C as Client>::Error>>
134 where
135 C: Client,
136 {
137 let token = access_token;
138 let validated = token.validate_token(http_client).await?;
139 if validated.user_id.is_some() {
140 return Err(ValidationError::InvalidToken(
141 "expected an app access token, got a user access token",
142 ));
143 }
144 Ok(Self::from_existing_unchecked(
145 token,
146 refresh_token.into(),
147 validated.client_id,
148 client_secret,
149 validated.scopes,
150 validated.expires_in,
151 ))
152 }
153
154 pub fn from_response(
156 response: crate::id::TwitchTokenResponse,
157 client_id: impl Into<ClientId>,
158 client_secret: impl Into<ClientSecret>,
159 ) -> AppAccessToken {
160 let expires_in = response.expires_in();
161 AppAccessToken::from_existing_unchecked(
162 response.access_token,
163 response.refresh_token,
164 client_id.into(),
165 client_secret,
166 response.scopes,
167 expires_in,
168 )
169 }
170
171 #[cfg(feature = "client")]
193 pub async fn get_app_access_token<C>(
194 http_client: &C,
195 client_id: ClientId,
196 client_secret: ClientSecret,
197 scopes: Vec<Scope>,
198 ) -> Result<AppAccessToken, AppAccessTokenError<<C as Client>::Error>>
199 where
200 C: Client,
201 {
202 let req = Self::get_app_access_token_request(&client_id, &client_secret, scopes);
203
204 let resp = http_client
205 .req(req)
206 .await
207 .map_err(AppAccessTokenError::Request)?;
208
209 let response = crate::id::TwitchTokenResponse::from_response(&resp)?;
210 let app_access = AppAccessToken::from_response(response, client_id, client_secret);
211
212 Ok(app_access)
213 }
214
215 pub fn get_app_access_token_request(
219 client_id: &ClientIdRef,
220 client_secret: &ClientSecretRef,
221 scopes: Vec<Scope>,
222 ) -> http::Request<Vec<u8>> {
223 use http::{HeaderMap, Method};
224 use std::collections::HashMap;
225 let scope: String = scopes
226 .iter()
227 .map(|s| s.to_string())
228 .collect::<Vec<_>>()
229 .join(" ");
230 let mut params = HashMap::new();
231 params.insert("client_id", client_id.as_str());
232 params.insert("client_secret", client_secret.secret());
233 params.insert("grant_type", "client_credentials");
234 params.insert("scope", &scope);
235
236 crate::construct_request(
237 &crate::TOKEN_URL,
238 ¶ms,
239 HeaderMap::new(),
240 Method::POST,
241 vec![],
242 )
243 }
244}