Skip to content

Commit de64e48

Browse files
committed
Merge branch 'main' into feature/conduits
2 parents 4171274 + 32efa3e commit de64e48

File tree

6 files changed

+105
-9
lines changed

6 files changed

+105
-9
lines changed

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ docs = [
6060
"sphinxcontrib_trio",
6161
]
6262
starlette = ["starlette", "uvicorn"]
63-
dev = ["ruff==0.11.2", "pyright==1.1.398", "isort"]
63+
dev = ["ruff==0.11.2", "pyright==1.1.402", "isort"]
6464

6565
[tool.ruff]
6666
line-length = 125
@@ -119,7 +119,7 @@ skip-magic-trailing-comma = false
119119
line-ending = "auto"
120120

121121
[tool.pyright]
122-
exclude = ["venv", "docs", "examples", "twitchio/__main__.py"]
122+
exclude = [".venv", "venv", "docs", "examples", "twitchio/__main__.py"]
123123
useLibraryCodeForTypes = true
124124
typeCheckingMode = "strict"
125125
reportImportCycles = false

twitchio/authentication/payloads.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,14 @@ class UserTokenPayload(BasePayload):
100100
A ``str`` or ``list[str]`` containing the scopes the user authenticated with.
101101
token_type: str
102102
The type of token provided. Usually ``bearer``.
103+
user_id: str | None
104+
An optional :class:`str` representing the ID of the User who authorized your application. This could be ``None``.
105+
user_login: str | None
106+
An optional :class:`str` representing the user name of the User who authorized your application.
107+
This could be ``None``.
103108
"""
104109

105-
__slots__ = ("access_token", "expires_in", "refresh_token", "scope", "token_type")
110+
__slots__ = ("_user_id", "_user_login", "access_token", "expires_in", "refresh_token", "scope", "token_type")
106111

107112
def __init__(self, raw: UserTokenResponse, /) -> None:
108113
super().__init__(raw)
@@ -112,6 +117,24 @@ def __init__(self, raw: UserTokenResponse, /) -> None:
112117
self.expires_in: int = raw["expires_in"]
113118
self.scope: str | list[str] = raw["scope"]
114119
self.token_type: str = raw["token_type"]
120+
self._user_id: str | None = None
121+
self._user_login: str | None = None
122+
123+
@property
124+
def user_id(self) -> str | None:
125+
return self._user_id
126+
127+
@user_id.setter
128+
def user_id(self, other: str) -> None:
129+
self._user_id = other
130+
131+
@property
132+
def user_login(self) -> str | None:
133+
return self._user_login
134+
135+
@user_login.setter
136+
def user_login(self, other: str) -> None:
137+
self._user_login = other
115138

116139

117140
class ClientCredentialsPayload(BasePayload):

twitchio/client.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,7 @@ def tokens(self) -> MappingProxyType[str, TokenMappingData]:
222222
223223
This method returns sensitive information such as user-tokens. You should take care not to expose these tokens.
224224
"""
225-
return MappingProxyType(self._http._tokens)
225+
return MappingProxyType(dict(self._http._tokens))
226226

227227
@property
228228
def bot_id(self) -> str | None:
@@ -434,7 +434,7 @@ async def login(self, *, token: str | None = None, load_tokens: bool = True, sav
434434
await self.load_tokens()
435435

436436
if self._bot_id:
437-
logger.debug("Fetching Clients self user for %r", self)
437+
logger.debug("Fetching Clients self user for %r", self.__class__.__name__)
438438
partial = PartialUser(id=self._bot_id, http=self._http)
439439
self._user = await partial.user() if self._fetch_self else partial
440440

@@ -628,7 +628,7 @@ async def close(self) -> None:
628628

629629
self._http.cleanup()
630630
self.__waiter.set()
631-
logger.debug("Cleanup completed on %r.", self)
631+
logger.debug("Cleanup completed on %r.", self.__class__.__name__)
632632

633633
async def wait_until_ready(self) -> None:
634634
"""|coro|

twitchio/ext/commands/bot.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from typing import TYPE_CHECKING, Any, TypeAlias, Unpack
3333

3434
from twitchio.client import AutoClient, Client
35+
from twitchio.user import PartialUser
3536

3637
from ...utils import _is_submodule
3738
from .context import Context
@@ -43,10 +44,11 @@
4344
if TYPE_CHECKING:
4445
from collections.abc import Callable, Coroutine, Iterable, Mapping
4546

47+
from twitchio.authentication.payloads import ClientCredentialsPayload, ValidateTokenPayload
4648
from twitchio.eventsub.subscriptions import SubscriptionPayload
4749
from twitchio.models.eventsub_ import ChannelPointsRedemptionAdd, ChannelPointsRedemptionUpdate, ChatMessage
4850
from twitchio.types_.eventsub import SubscriptionResponse
49-
from twitchio.user import PartialUser
51+
from twitchio.user import User
5052

5153
from .components import Component
5254
from .types_ import AutoBotOptions, BotOptions
@@ -176,6 +178,7 @@ def __init__(
176178
self._components: dict[str, Component] = {}
177179
self._base_converter: _BaseConverter = _BaseConverter(self)
178180
self.__modules: dict[str, types.ModuleType] = {}
181+
self._owner: User | None = None
179182

180183
@property
181184
def bot_id(self) -> str:
@@ -204,6 +207,64 @@ def owner_id(self) -> str | None:
204207
"""
205208
return self._owner_id
206209

210+
@property
211+
def owner(self) -> User | None:
212+
"""Property which returns the :class:`~twitchio.User` associated with with the User who owns this bot.
213+
214+
Could be ``None`` if no ``owner_id`` was passed to the Bot constructor or the request failed.
215+
Passing a ``owner_id`` is highly recommended.
216+
217+
.. important::
218+
219+
If ``owner_id`` has not been passed to the constructor of this :class:`.Bot` this will return ``None``.
220+
"""
221+
return self._owner
222+
223+
async def login(self, *, token: str | None = None, load_tokens: bool = True, save_tokens: bool = True) -> None:
224+
if self._login_called:
225+
return
226+
227+
self._login_called = True
228+
self._save_tokens = save_tokens
229+
230+
if not self._http.client_id:
231+
raise RuntimeError('Expected a valid "client_id", instead received: %s', self._http.client_id)
232+
233+
if not token and not self._http.client_secret:
234+
raise RuntimeError(f'Expected a valid "client_secret", instead received: {self._http.client_secret}')
235+
236+
if not token:
237+
payload: ClientCredentialsPayload = await self._http.client_credentials_token()
238+
validated: ValidateTokenPayload = await self._http.validate_token(payload.access_token)
239+
token = payload.access_token
240+
241+
logger.info("Generated App Token for Client-ID: %s", validated.client_id)
242+
243+
self._http._app_token = token
244+
245+
if load_tokens:
246+
async with self._http._token_lock:
247+
await self.load_tokens()
248+
249+
if self._bot_id:
250+
logger.debug("Fetching Clients self user for %r", self.__class__.__name__)
251+
partial = PartialUser(id=self._bot_id, http=self._http)
252+
self._user = await partial.user() if self._fetch_self else partial
253+
254+
if self._owner_id:
255+
logger.debug("Fetching owner User for %r", self.__class__.__name__)
256+
partial = PartialUser(id=self._owner_id, http=self._http)
257+
258+
try:
259+
user = await partial.user()
260+
except Exception:
261+
logger.warning("Failed to retrieve the Owner User during startup. Owner will be None.")
262+
else:
263+
self._owner = user
264+
265+
await self.setup_hook()
266+
self._setup_called = True
267+
207268
async def close(self, **options: Any) -> None:
208269
for module in tuple(self.__modules):
209270
try:

twitchio/web/aio_adapter.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333

3434
from aiohttp import web
3535

36+
from twitchio.authentication.payloads import ValidateTokenPayload
37+
3638
from ..authentication import Scopes
3739
from ..eventsub.subscriptions import _SUB_MAPPING
3840
from ..exceptions import HTTPException
@@ -44,7 +46,7 @@
4446
if TYPE_CHECKING:
4547
from ssl import SSLContext
4648

47-
from ..authentication import AuthorizationURLPayload, UserTokenPayload
49+
from ..authentication import AuthorizationURLPayload, UserTokenPayload, ValidateTokenPayload
4850
from ..client import Client
4951
from ..types_.eventsub import EventSubHeaders
5052

@@ -324,7 +326,12 @@ async def fetch_token(self, request: web.Request) -> FetchTokenPayload:
324326
status: int = e.status
325327
return FetchTokenPayload(status=status, response=web.Response(status=status), exception=e)
326328

329+
validated: ValidateTokenPayload = await self.client._http.validate_token(resp.access_token)
330+
resp._user_id = validated.user_id
331+
resp._user_login = validated.login
332+
327333
self.client.dispatch(event="oauth_authorized", payload=resp)
334+
328335
return FetchTokenPayload(
329336
status=20,
330337
response=web.Response(body="Success. You can leave this page.", status=200),

twitchio/web/starlette_adapter.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
if TYPE_CHECKING:
4848
from starlette.requests import Request
4949

50-
from ..authentication import AuthorizationURLPayload, UserTokenPayload
50+
from ..authentication import AuthorizationURLPayload, UserTokenPayload, ValidateTokenPayload
5151
from ..client import Client
5252
from ..types_.eventsub import EventSubHeaders
5353

@@ -347,7 +347,12 @@ async def fetch_token(self, request: Request) -> FetchTokenPayload:
347347
status: int = e.status
348348
return FetchTokenPayload(status=status, response=Response(status_code=status), exception=e)
349349

350+
validated: ValidateTokenPayload = await self.client._http.validate_token(resp.access_token)
351+
resp._user_id = validated.user_id
352+
resp._user_login = validated.login
353+
350354
self.client.dispatch(event="oauth_authorized", payload=resp)
355+
351356
return FetchTokenPayload(
352357
status=200,
353358
response=Response(content="Success. You can leave this page.", status_code=200),

0 commit comments

Comments
 (0)