Skip to content

Commit 9230fa7

Browse files
committed
Add DCF methods, expose http and remove deprecated asyncio method.
1 parent 911d0f6 commit 9230fa7

File tree

1 file changed

+119
-2
lines changed

1 file changed

+119
-2
lines changed

twitchio/client.py

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from __future__ import annotations
2626

2727
import asyncio
28+
import inspect
2829
import logging
2930
import math
3031
from collections import defaultdict
@@ -68,6 +69,7 @@
6869
from .types_.conduits import ShardUpdateRequest
6970
from .types_.eventsub import ShardStatus, SubscriptionCreateTransport, SubscriptionResponse, _SubscriptionData
7071
from .types_.options import AutoClientOptions, ClientOptions, WaitPredicateT
72+
from .types_.responses import DeviceCodeFlowResponse
7173
from .types_.tokens import TokenMappingData
7274

7375

@@ -128,7 +130,7 @@ def __init__(
128130
self,
129131
*,
130132
client_id: str,
131-
client_secret: str,
133+
client_secret: str | None = None,
132134
bot_id: str | None = None,
133135
**options: Unpack[ClientOptions],
134136
) -> None:
@@ -188,6 +190,40 @@ def adapter(self) -> BaseAdapter[Any]:
188190
currently running."""
189191
return self._adapter
190192

193+
@property
194+
def http(self) -> ManagedHTTPClient:
195+
"""Property exposing the internal :class:`~twitchio.ManagedHTTPClient` used for requests to the Twitch API.
196+
197+
.. warning::
198+
199+
Altering or changing this class during runtime may have unwanted side-effects. It is exposed for developer
200+
easabilty, especially when OAuth or Device Code Flow methods are required. It is not intended to replace the use
201+
of the built-in methods of the :class:`~twitchio.Client` or other models.
202+
"""
203+
return self._http
204+
205+
async def device_code_flow(self, *, scopes: Scopes | None = None) -> DeviceCodeFlowResponse:
206+
"""|coro|
207+
208+
.. warning::
209+
210+
It's not intended to use DCF when storing a ``client-secret`` is a safe and practical option.
211+
DCF is intended to be used on user devices where storing your ``client-secret`` is not possible.
212+
213+
.. note::
214+
215+
When using tokens generated through DCF, the only ``EventSub`` transport available is traditional websockets.
216+
Using ``Conduits`` and ``Webhooks`` are not available as they require a ``App Token``.
217+
218+
Method which starts a Twitch Device Code Flow.
219+
220+
The DCF (Device Code Flow) is used to obtain an access/refresh token pair for use on client-side applications where
221+
storing a ``client-secret`` would be unsafe E.g. a users device (Phone, TV, etc...).
222+
223+
When using a token
224+
"""
225+
return await self._http.device_code_flow(scopes=scopes)
226+
191227
async def set_adapter(self, adapter: BaseAdapter[Any]) -> None:
192228
"""|coro|
193229
@@ -392,6 +428,87 @@ async def setup_hook(self) -> None:
392428
"""
393429
...
394430

431+
async def login_dcf(
432+
self,
433+
*,
434+
load_token: bool = True,
435+
save_token: bool = True,
436+
scopes: Scopes | None = None,
437+
force_flow: bool = False,
438+
) -> DeviceCodeFlowResponse | None:
439+
if self._login_called:
440+
return
441+
442+
self._login_called = True
443+
self._save_tokens = save_token
444+
445+
if not self._http.client_id:
446+
raise RuntimeError('Expected a valid "client_id", instead received: %s', self._http.client_id)
447+
448+
self._http._app_token = None
449+
450+
if load_token and not force_flow:
451+
async with self._http._token_lock:
452+
await self.load_tokens()
453+
else:
454+
self._http._has_loaded = True
455+
456+
await self._setup()
457+
458+
if not self._http._tokens:
459+
return await self.device_code_flow(scopes=scopes)
460+
461+
async def start_dcf(
462+
self,
463+
*,
464+
device_code: str | None = None,
465+
interval: int = 5,
466+
timeout: int | None = 90,
467+
scopes: Scopes | None = None,
468+
) -> None:
469+
if not self._login_called:
470+
raise RuntimeError('Client failed to start: "login_dcf" must be called before "start_dcf".')
471+
472+
self.__waiter.clear()
473+
474+
try:
475+
mapping = list(self._http._tokens.values())
476+
pair = mapping[0]
477+
token = pair["token"]
478+
refresh = pair["refresh"]
479+
except (IndexError, KeyError):
480+
token = ""
481+
refresh = ""
482+
483+
if device_code:
484+
async with asyncio.timeout(timeout):
485+
resp = await self._http.device_code_authorization(device_code=device_code, interval=interval, scopes=scopes)
486+
487+
token = resp["access_token"]
488+
refresh = resp["refresh_token"]
489+
490+
if not token or not refresh:
491+
raise RuntimeError(
492+
"Unable to start Client: No DCF token pair was able to be loaded. Try force running the flow."
493+
)
494+
495+
validated = await self.add_token(token=token, refresh=refresh)
496+
self._http._app_token = token
497+
498+
user = await self.fetch_user(id=validated.user_id)
499+
if not user:
500+
raise RuntimeError("Unable to fetch associated user with DCF token.")
501+
502+
self._bot_id = user.id
503+
self.dispatch("ready")
504+
self._ready_event.set()
505+
506+
try:
507+
await self.__waiter.wait()
508+
finally:
509+
self._ready_event.clear()
510+
await self.close()
511+
395512
async def login(self, *, token: str | None = None, load_tokens: bool = True, save_tokens: bool = True) -> None:
396513
"""|coro|
397514
@@ -884,7 +1001,7 @@ def add_listener(self, listener: Callable[..., Coroutine[Any, Any, None]], *, ev
8841001
if name == "event_":
8851002
raise ValueError('Listener and event names cannot be named "event_".')
8861003

887-
if not asyncio.iscoroutinefunction(listener):
1004+
if not inspect.iscoroutinefunction(listener):
8881005
raise TypeError("Listeners and Events must be coroutines.")
8891006

8901007
self._listeners[name].add(listener)

0 commit comments

Comments
 (0)