Skip to content

Commit f0b63bd

Browse files
committed
Merge master into main.
2 parents 47ec593 + 91b3b62 commit f0b63bd

File tree

6 files changed

+199
-23
lines changed

6 files changed

+199
-23
lines changed

docs/references/web.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,15 @@ Web/OAuth Reference
1212
.. attributetable:: twitchio.web.StarletteAdapter
1313

1414
.. autoclass:: twitchio.web.StarletteAdapter
15-
:members:
15+
:members:
16+
17+
Models and Payloads
18+
===================
19+
20+
.. attributetable:: twitchio.web.FetchTokenPayload
21+
22+
.. autoclass:: twitchio.web.FetchTokenPayload()
23+
24+
.. attributetable:: twitchio.authentication.UserTokenPayload
25+
26+
.. autoclass:: twitchio.authentication.UserTokenPayload()

twitchio/authentication/payloads.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,24 @@ def __init__(self, raw: ValidateTokenResponse, /) -> None:
8484

8585

8686
class UserTokenPayload(BasePayload):
87+
"""OAuth model received when a user successfully authenticates your application on Twitch.
88+
89+
This is a raw container class.
90+
91+
Attributes
92+
----------
93+
access_token: str
94+
The user access token.
95+
refresh_token: str
96+
The user refresh token for this access token.
97+
expires_in: int
98+
The amount of time this token is valid before expiring as seconds.
99+
scope: str | list[str]
100+
A ``str`` or ``list[str]`` containing the scopes the user authenticated with.
101+
token_type: str
102+
The type of token provided. Usually ``bearer``.
103+
"""
104+
87105
__slots__ = ("access_token", "expires_in", "refresh_token", "scope", "token_type")
88106

89107
def __init__(self, raw: UserTokenResponse, /) -> None:

twitchio/web/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
from ..utils import ColorFormatter
3030
from .aio_adapter import AiohttpAdapter as AiohttpAdapter
31+
from .utils import FetchTokenPayload as FetchTokenPayload
3132

3233

3334
handler = logging.StreamHandler()

twitchio/web/aio_adapter.py

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,11 @@
3535

3636
from ..authentication import Scopes
3737
from ..eventsub.subscriptions import _SUB_MAPPING
38+
from ..exceptions import HTTPException
3839
from ..models.eventsub_ import SubscriptionRevoked, create_event_instance
3940
from ..types_.eventsub import EventSubHeaders
4041
from ..utils import _from_json, parse_timestamp # type: ignore
41-
from .utils import MESSAGE_TYPES, BaseAdapter, verify_message
42+
from .utils import MESSAGE_TYPES, BaseAdapter, FetchTokenPayload, verify_message
4243

4344

4445
if TYPE_CHECKING:
@@ -271,30 +272,85 @@ async def eventsub_callback(self, request: web.Request) -> web.Response:
271272

272273
return web.Response(status=204)
273274

274-
async def fetch_token(self, request: web.Request) -> web.Response:
275+
async def fetch_token(self, request: web.Request) -> FetchTokenPayload:
276+
"""This method handles sending the provided code to Twitch to receive a User Access and Refresh Token pair, and
277+
later, if successful, dispatches :func:`~twitchio.event_oauth_authorized`.
278+
279+
To call this coroutine you should pass the request received in the :meth:`oauth_callback`. This method is called by
280+
default, however when overriding :meth:`oauth_callback` you should always call this method.
281+
282+
Parameters
283+
----------
284+
request: aiohttp.web.Request
285+
The request received in :meth:`oauth_callback`.
286+
287+
Returns
288+
-------
289+
FetchTokenPayload
290+
The payload containing various information about the authentication request to Twitch.
291+
"""
275292
if "code" not in request.query:
276-
return web.Response(status=400)
293+
return FetchTokenPayload(400, response=web.Response(status=400, text="No 'code' parameter provided."))
277294

278295
try:
279-
payload: UserTokenPayload = await self.client._http.user_access_token(
296+
resp: UserTokenPayload = await self.client._http.user_access_token(
280297
request.query["code"],
281298
redirect_uri=self.redirect_url,
282299
)
283-
except Exception as e:
300+
except HTTPException as e:
284301
logger.error("Exception raised while fetching Token in <%s>: %s", self.__class__.__qualname__, e)
285-
return web.Response(status=500)
302+
status: int = e.status
303+
return FetchTokenPayload(status=status, response=web.Response(status=status), exception=e)
286304

287-
self.client.dispatch(event="oauth_authorized", payload=payload)
288-
return web.Response(body="Success. You can leave this page.", status=200)
305+
self.client.dispatch(event="oauth_authorized", payload=resp)
306+
return FetchTokenPayload(
307+
status=20,
308+
response=web.Response(body="Success. You can leave this page.", status=200),
309+
payload=resp,
310+
)
289311

290312
async def oauth_callback(self, request: web.Request) -> web.Response:
313+
"""Default route callback for the OAuth Authentication redirect URL.
314+
315+
You can override this method to alter the responses sent to the user.
316+
317+
This callback should always return a valid response. See: `aiohttp docs <https://docs.aiohttp.org/en/stable/web.html>`_
318+
for more information.
319+
320+
.. important::
321+
322+
You should always call :meth:`.fetch_token` when overriding this method.
323+
324+
Parameters
325+
----------
326+
request: aiohttp.web.Request
327+
The original request received via aiohttp.
328+
329+
Examples
330+
--------
331+
332+
.. code:: python3
333+
334+
async def oauth_callback(self, request: web.Request) -> web.Response:
335+
payload: FetchTokenPayload = await self.fetch_token(request)
336+
337+
# Change the default success response to no content...
338+
if payload.status == 200:
339+
return web.Response(status=204)
340+
341+
# Return the default error responses...
342+
return payload.response
343+
"""
291344
logger.debug("Received OAuth callback request in <%s>.", self.oauth_callback.__qualname__)
292345

293-
response: web.Response = await self.fetch_token(request)
294-
return response
346+
payload: FetchTokenPayload = await self.fetch_token(request)
347+
if not isinstance(payload.response, web.Response):
348+
raise ValueError(f"Responses in AiohttpAdapter should be {type(web.Response)!r} not {type(payload.response)!r}")
349+
350+
return payload.response
295351

296352
async def oauth_redirect(self, request: web.Request) -> web.Response:
297-
scopes: str | None = request.query.get("scopes", None)
353+
scopes: str | None = request.query.get("scopes", request.query.get("scope", None))
298354
force_verify: bool = request.query.get("force_verify", "false").lower() == "true"
299355

300356
if not scopes:

twitchio/web/starlette_adapter.py

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,10 +38,11 @@
3838

3939
from ..authentication import Scopes
4040
from ..eventsub.subscriptions import _SUB_MAPPING
41+
from ..exceptions import HTTPException
4142
from ..models.eventsub_ import SubscriptionRevoked, create_event_instance
4243
from ..types_.eventsub import EventSubHeaders
4344
from ..utils import _from_json, parse_timestamp # type: ignore
44-
from .utils import MESSAGE_TYPES, BaseAdapter, verify_message
45+
from .utils import MESSAGE_TYPES, BaseAdapter, FetchTokenPayload, verify_message
4546

4647

4748
if TYPE_CHECKING:
@@ -283,27 +284,82 @@ async def eventsub_callback(self, request: Request) -> Response:
283284

284285
return Response(status_code=204)
285286

286-
async def fetch_token(self, request: Request) -> Response:
287+
async def fetch_token(self, request: Request) -> FetchTokenPayload:
288+
"""This method handles sending the provided code to Twitch to receive a User Access and Refresh Token pair, and
289+
later, if successful, dispatches :func:`~twitchio.event_oauth_authorized`.
290+
291+
To call this coroutine you should pass the request received in the :meth:`oauth_callback`. This method is called by
292+
default, however when overriding :meth:`oauth_callback` you should always call this method.
293+
294+
Parameters
295+
----------
296+
request: starlette.requests.Request
297+
The request received in :meth:`oauth_callback`.
298+
299+
Returns
300+
-------
301+
FetchTokenPayload
302+
The payload containing various information about the authentication request to Twitch.
303+
"""
287304
if "code" not in request.query_params:
288-
return Response(status_code=400)
305+
return FetchTokenPayload(400, response=Response(status_code=400, content="No 'code' parameter provided."))
289306

290307
try:
291-
payload: UserTokenPayload = await self.client._http.user_access_token(
308+
resp: UserTokenPayload = await self.client._http.user_access_token(
292309
request.query_params["code"],
293310
redirect_uri=self.redirect_url,
294311
)
295-
except Exception as e:
312+
except HTTPException as e:
296313
logger.error("Exception raised while fetching Token in <%s>: %s", self.__class__.__qualname__, e)
297-
return Response(status_code=500)
298-
299-
self.client.dispatch(event="oauth_authorized", payload=payload)
300-
return Response("Success. You can leave this page.", status_code=200)
314+
status: int = e.status
315+
return FetchTokenPayload(status=status, response=Response(status_code=status), exception=e)
316+
317+
self.client.dispatch(event="oauth_authorized", payload=resp)
318+
return FetchTokenPayload(
319+
status=20,
320+
response=Response(content="Success. You can leave this page.", status_code=200),
321+
payload=resp,
322+
)
301323

302324
async def oauth_callback(self, request: Request) -> Response:
325+
"""Default route callback for the OAuth Authentication redirect URL.
326+
327+
You can override this method to alter the responses sent to the user.
328+
329+
This callback should always return a valid response. See: `Starlette Responses <https://www.starlette.io/responses/>`_
330+
for available response types.
331+
332+
.. important::
333+
334+
You should always call :meth:`.fetch_token` when overriding this method.
335+
336+
Parameters
337+
----------
338+
request: starlette.requests.Request
339+
The original request received via Starlette.
340+
341+
Examples
342+
--------
343+
344+
.. code:: python3
345+
346+
async def oauth_callback(self, request: Request) -> Response:
347+
payload: FetchTokenPayload = await self.fetch_token(request)
348+
349+
# Change the default success response...
350+
if payload.status == 200:
351+
return HTMLResponse(status_code=200, "<h1>Success!</h1>")
352+
353+
# Return the default error responses...
354+
return payload.response
355+
"""
303356
logger.debug("Received OAuth callback request in <%s>.", self.oauth_callback.__qualname__)
304357

305-
response: Response = await self.fetch_token(request)
306-
return response
358+
payload: FetchTokenPayload = await self.fetch_token(request)
359+
if not isinstance(payload.response, Response):
360+
raise ValueError(f"Responses in StarlettepAdapter should be {type(Response)!r} not {type(payload.response)!r}")
361+
362+
return payload.response
307363

308364
async def oauth_redirect(self, request: Request) -> Response:
309365
scopes: str | None = request.query_params.get("scopes", None)

twitchio/web/utils.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,11 @@
3737
import asyncio
3838

3939
from starlette.requests import Request
40+
from starlette.responses import Response
4041

42+
from ..authentication import UserTokenPayload
4143
from ..client import Client
44+
from ..exceptions import HTTPException
4245
from ..types_.eventsub import EventSubHeaders
4346

4447

@@ -48,6 +51,37 @@
4851
MESSAGE_TYPES = ["notification", "webhook_callback_verification", "revocation"]
4952

5053

54+
class FetchTokenPayload:
55+
"""Payload model returned via :meth:`twitchio.web.StarletteAdapter.fetch_token` and
56+
:meth:`twitchio.web.AiohttpAdapter.fetch_token`
57+
58+
Attributes
59+
----------
60+
status: int
61+
The status code returned while trying to authenticate a user on Twitch. A status of ``200`` indicates a success.
62+
response: web.Response | starlette.responses.Response
63+
The response TwitchIO sends by default to the user after trying to authenticate via OAuth.
64+
payload: :class:`twitchio.authentication.UserTokenPayload`
65+
The payload received from Twitch when a user successfully authenticates via OAuth. Will be ``None`` if a non ``200``
66+
status code is returned.
67+
exception: :class:`twitchio.HTTPException` | None
68+
The exception raised while trying to authenticate a user. Could be ``None`` if no exception occurred.
69+
"""
70+
71+
def __init__(
72+
self,
73+
status: int,
74+
*,
75+
response: web.Response | Response,
76+
payload: UserTokenPayload | None = None,
77+
exception: HTTPException | None = None,
78+
) -> None:
79+
self.status = status
80+
self.response = response
81+
self.payload = payload
82+
self.exception = exception
83+
84+
5185
class BaseAdapter(abc.ABC):
5286
client: Client
5387
_runner_task: asyncio.Task[None] | None

0 commit comments

Comments
 (0)