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
55#[cfg_attr(feature = "client", async_trait::async_trait)]
56impl TwitchToken for AppAccessToken {
57 fn token_type() -> super::BearerTokenType { super::BearerTokenType::AppAccessToken }
58
59 fn client_id(&self) -> &ClientId { &self.client_id }
60
61 fn token(&self) -> &AccessToken { &self.access_token }
62
63 fn login(&self) -> Option<&UserNameRef> { None }
64
65 fn user_id(&self) -> Option<&UserIdRef> { None }
66
67 #[cfg(feature = "client")]
68 async fn refresh_token<'a, C>(
69 &mut self,
70 http_client: &'a C,
71 ) -> Result<(), RefreshTokenError<<C as Client>::Error>>
72 where
73 C: Client,
74 {
75 let (access_token, expires_in, refresh_token) =
76 if let Some(token) = self.refresh_token.take() {
77 token
78 .refresh_token(http_client, &self.client_id, Some(&self.client_secret))
79 .await?
80 } else {
81 return Err(RefreshTokenError::NoRefreshToken);
82 };
83 self.access_token = access_token;
84 self.expires_in = expires_in;
85 self.refresh_token = refresh_token;
86 self.struct_created = Instant::now();
87 Ok(())
88 }
89
90 fn expires_in(&self) -> std::time::Duration {
91 self.expires_in
92 .checked_sub(self.struct_created.elapsed())
93 .unwrap_or_default()
94 }
95
96 fn scopes(&self) -> &[Scope] { self.scopes.as_slice() }
97}
98
99impl AppAccessToken {
100 pub fn from_existing_unchecked(
109 access_token: AccessToken,
110 refresh_token: impl Into<Option<RefreshToken>>,
111 client_id: impl Into<ClientId>,
112 client_secret: impl Into<ClientSecret>,
113 scopes: Option<Vec<Scope>>,
114 expires_in: Option<std::time::Duration>,
115 ) -> AppAccessToken {
116 AppAccessToken {
117 access_token,
118 refresh_token: refresh_token.into(),
119 client_id: client_id.into(),
120 client_secret: client_secret.into(),
121 expires_in: expires_in.unwrap_or_default(),
122 struct_created: Instant::now(),
123 scopes: scopes.unwrap_or_default(),
124 }
125 }
126
127 #[cfg(feature = "client")]
129 pub async fn from_existing<C>(
130 http_client: &C,
131 access_token: AccessToken,
132 refresh_token: impl Into<Option<RefreshToken>>,
133 client_secret: ClientSecret,
134 ) -> Result<AppAccessToken, ValidationError<<C as Client>::Error>>
135 where
136 C: Client,
137 {
138 let token = access_token;
139 let validated = token.validate_token(http_client).await?;
140 if validated.user_id.is_some() {
141 return Err(ValidationError::InvalidToken(
142 "expected an app access token, got a user access token",
143 ));
144 }
145 Ok(Self::from_existing_unchecked(
146 token,
147 refresh_token.into(),
148 validated.client_id,
149 client_secret,
150 validated.scopes,
151 validated.expires_in,
152 ))
153 }
154
155 pub fn from_response(
157 response: crate::id::TwitchTokenResponse,
158 client_id: impl Into<ClientId>,
159 client_secret: impl Into<ClientSecret>,
160 ) -> AppAccessToken {
161 let expires_in = response.expires_in();
162 AppAccessToken::from_existing_unchecked(
163 response.access_token,
164 response.refresh_token,
165 client_id.into(),
166 client_secret,
167 response.scopes,
168 expires_in,
169 )
170 }
171
172 #[cfg(feature = "client")]
194 pub async fn get_app_access_token<C>(
195 http_client: &C,
196 client_id: ClientId,
197 client_secret: ClientSecret,
198 scopes: Vec<Scope>,
199 ) -> Result<AppAccessToken, AppAccessTokenError<<C as Client>::Error>>
200 where
201 C: Client,
202 {
203 let req = Self::get_app_access_token_request(&client_id, &client_secret, scopes);
204
205 let resp = http_client
206 .req(req)
207 .await
208 .map_err(AppAccessTokenError::Request)?;
209
210 let response = crate::id::TwitchTokenResponse::from_response(&resp)?;
211 let app_access = AppAccessToken::from_response(response, client_id, client_secret);
212
213 Ok(app_access)
214 }
215
216 pub fn get_app_access_token_request(
220 client_id: &ClientIdRef,
221 client_secret: &ClientSecretRef,
222 scopes: Vec<Scope>,
223 ) -> http::Request<Vec<u8>> {
224 use http::{HeaderMap, Method};
225 use std::collections::HashMap;
226 let scope: String = scopes
227 .iter()
228 .map(|s| s.to_string())
229 .collect::<Vec<_>>()
230 .join(" ");
231 let mut params = HashMap::new();
232 params.insert("client_id", client_id.as_str());
233 params.insert("client_secret", client_secret.secret());
234 params.insert("grant_type", "client_credentials");
235 params.insert("scope", &scope);
236
237 crate::construct_request(
238 &crate::TOKEN_URL,
239 ¶ms,
240 HeaderMap::new(),
241 Method::POST,
242 vec![],
243 )
244 }
245}