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