2424
2525from __future__ import annotations
2626
27+ import asyncio
2728import secrets
2829import urllib .parse
2930from typing import TYPE_CHECKING , ClassVar
3031
32+ import twitchio
33+
34+ from ..enums import DeviceCodeRejection
3135from ..http import HTTPClient , Route
3236from ..utils import MISSING
3337from .payloads import *
3943 from ..types_ .responses import (
4044 AuthorizationURLResponse ,
4145 ClientCredentialsResponse ,
46+ DeviceCodeFlowResponse ,
47+ DeviceCodeTokenResponse ,
4248 RefreshTokenResponse ,
4349 UserTokenResponse ,
4450 ValidateTokenResponse ,
@@ -53,7 +59,7 @@ def __init__(
5359 self ,
5460 * ,
5561 client_id : str ,
56- client_secret : str ,
62+ client_secret : str | None = None ,
5763 redirect_uri : str | None = None ,
5864 scopes : Scopes | None = None ,
5965 session : aiohttp .ClientSession = MISSING ,
@@ -66,6 +72,27 @@ def __init__(
6672 self .scopes = scopes
6773
6874 async def validate_token (self , token : str , / ) -> ValidateTokenPayload :
75+ """|coro|
76+
77+ Method which validates the provided token.
78+
79+ Parameters
80+ ----------
81+ token: :class:`str`
82+ The token to attempt to validate.
83+
84+ Returns
85+ -------
86+ ValidateTokenPayload
87+ The payload received from Twitch if no HTTPException was raised.
88+
89+ Raises
90+ ------
91+ HTTPException
92+ An error occurred during a request to Twitch.
93+ HTTPException
94+ Bad or invalid token provided.
95+ """
6996 token = token .removeprefix ("Bearer " ).removeprefix ("OAuth " )
7097
7198 headers : dict [str , str ] = {"Authorization" : f"OAuth { token } " }
@@ -108,6 +135,20 @@ async def user_access_token(self, code: str, /, *, redirect_uri: str | None = No
108135 return UserTokenPayload (data )
109136
110137 async def revoke_token (self , token : str , / ) -> None :
138+ """|coro|
139+
140+ Method to revoke the authorization of a provided token.
141+
142+ Parameters
143+ ----------
144+ token: :class:`str`
145+ The token to revoke authorization from. The token will be invalid and cannot be used after revocation.
146+
147+ Raises
148+ ------
149+ HTTPException
150+ An error occurred during a request to Twitch.
151+ """
111152 params = self ._create_params ({"token" : token })
112153
113154 route : Route = Route ("POST" , "/oauth2/revoke" , use_id = True , headers = self .CONTENT_TYPE_HEADER , params = params )
@@ -121,6 +162,57 @@ async def client_credentials_token(self) -> ClientCredentialsPayload:
121162
122163 return ClientCredentialsPayload (data )
123164
165+ async def device_code_flow (self , * , scopes : Scopes | None = None ) -> DeviceCodeFlowResponse :
166+ scopes = scopes or self .scopes
167+ if not scopes :
168+ raise ValueError ('"scopes" is a required parameter or attribute which is missing.' )
169+
170+ params = self ._create_params ({"scopes" : scopes .urlsafe ()}, device_code = True )
171+ route : Route = Route ("POST" , "/oauth2/device" , use_id = True , headers = self .CONTENT_TYPE_HEADER , params = params )
172+
173+ return await self .request_json (route )
174+
175+ async def device_code_authorization (
176+ self ,
177+ * ,
178+ scopes : Scopes | None = None ,
179+ device_code : str ,
180+ interval : int = 5 ,
181+ ) -> DeviceCodeTokenResponse :
182+ scopes = scopes or self .scopes
183+ if not scopes :
184+ raise ValueError ('"scopes" is a required parameter or attribute which is missing.' )
185+
186+ params = self ._create_params (
187+ {
188+ "scopes" : scopes .urlsafe (),
189+ "device_code" : device_code ,
190+ "grant_type" : "urn:ietf:params:oauth:grant-type:device_code" ,
191+ },
192+ device_code = True ,
193+ )
194+
195+ route : Route = Route ("POST" , "/oauth2/token" , use_id = True , params = params )
196+
197+ while True :
198+ try :
199+ resp = await self .request_json (route )
200+ except twitchio .HTTPException as e :
201+ if e .status != 400 :
202+ msg = "Unknown error during Device Code Authorization."
203+ raise twitchio .DeviceCodeFlowException (msg , original = e ) from e
204+
205+ message = e .extra .get ("message" , "" ).lower ()
206+
207+ if message != "authorization_pending" :
208+ msg = f"An error occurred during Device Code Authorization: { message .upper ()} ."
209+ raise twitchio .DeviceCodeFlowException (original = e , reason = DeviceCodeRejection (message ))
210+
211+ await asyncio .sleep (interval )
212+ continue
213+
214+ return resp
215+
124216 def get_authorization_url (
125217 self ,
126218 * ,
@@ -163,10 +255,11 @@ def get_authorization_url(
163255 payload : AuthorizationURLPayload = AuthorizationURLPayload (data )
164256 return payload
165257
166- def _create_params (self , extra_params : dict [str , str ]) -> dict [str , str ]:
167- params = {
168- "client_id" : self .client_id ,
169- "client_secret" : self .client_secret ,
170- }
258+ def _create_params (self , extra_params : dict [str , str ], * , device_code : bool = False ) -> dict [str , str ]:
259+ params = {"client_id" : self .client_id }
260+
261+ if not device_code and self .client_secret :
262+ params ["client_secret" ] = self .client_secret
263+
171264 params .update (extra_params )
172265 return params
0 commit comments