Skip to content

Commit 7b41c63

Browse files
committed
Add event_token_refreshed
1 parent 27e35cc commit 7b41c63

File tree

6 files changed

+104
-4
lines changed

6 files changed

+104
-4
lines changed

docs/references/events.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,15 @@ Client Events
316316
Event dispatched when an exception is raised inside of a dispatched event.
317317

318318
:param twitchio.EventErrorPayload payload: The payload containing information about the event and exception raised.
319+
320+
.. py:function:: event_token_refreshed(payload: twitchio.TokenRefreshedPayload) -> None:
321+
:async:
322+
323+
Event dispatched when a token managed by the :class:`~twitchio.Client` is successfully refreshed.
324+
325+
You can use this event to update the stored token locally as they are refreshed.
326+
327+
:param TokenRefreshedPayload payload: The payload containing various information about the refreshed token.
319328

320329
.. py:function:: event_oauth_authorized(payload: twitchio.authentication.UserTokenPayload) -> None
321330
:async:
@@ -1297,4 +1306,9 @@ Payloads
12971306
.. attributetable:: twitchio.EventErrorPayload
12981307

12991308
.. autoclass:: twitchio.EventErrorPayload()
1309+
:members:
1310+
1311+
.. attributetable:: twitchio.TokenRefreshedPayload
1312+
1313+
.. autoclass:: twitchio.TokenRefreshedPayload()
13001314
:members:

twitchio/authentication/tokens.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
SOFTWARE.
2323
"""
2424

25+
from __future__ import annotations
26+
2527
import asyncio
2628
import datetime
2729
import json
@@ -36,6 +38,7 @@
3638
from ..backoff import Backoff
3739
from ..exceptions import HTTPException, InvalidTokenException
3840
from ..http import HTTPAsyncIterator, PaginatedConverter
41+
from ..payloads import TokenRefreshedPayload
3942
from ..types_.tokens import TokenMappingData
4043
from ..utils import MISSING
4144
from .oauth import OAuth
@@ -44,7 +47,8 @@
4447

4548

4649
if TYPE_CHECKING:
47-
from ..types_.tokens import TokenMapping
50+
from ..client import Client
51+
from ..types_.tokens import TokenMapping, _TokenRefreshedPayload
4852
from .payloads import RefreshTokenPayload
4953

5054

@@ -64,6 +68,7 @@ def __init__(
6468
scopes: Scopes | None = None,
6569
session: aiohttp.ClientSession = MISSING,
6670
nested_key: str | None = None,
71+
client: Client | None = None,
6772
) -> None:
6873
super().__init__(
6974
client_id=client_id,
@@ -89,6 +94,21 @@ def __init__(
8994
self._backoff: Backoff = Backoff(base=3, maximum_time=90)
9095

9196
self._validate_task: asyncio.Task[None] | None = None
97+
self._client = client
98+
99+
def _dispatch_event(self, user_id: str, payload: RefreshTokenPayload) -> None:
100+
if not self._client:
101+
return
102+
103+
data: _TokenRefreshedPayload = {
104+
"user_id": user_id,
105+
"refresh_token": payload.refresh_token,
106+
"token": payload.access_token,
107+
"scopes": Scopes(payload.scope),
108+
"expires_in": payload.expires_in,
109+
}
110+
111+
self._client.dispatch("token_refreshed", TokenRefreshedPayload(data=data))
92112

93113
async def _attempt_refresh_on_add(self, token: str, refresh: str) -> ValidateTokenPayload:
94114
logger.debug("Token was invalid when attempting to add it to the token manager. Attempting to refresh.")
@@ -118,6 +138,7 @@ async def _attempt_refresh_on_add(self, token: str, refresh: str) -> ValidateTok
118138
"last_validated": datetime.datetime.now().isoformat(),
119139
}
120140

141+
self._dispatch_event(valid_resp.user_id, resp)
121142
logger.info('Token successfully added to TokenManager after refresh: "%s"', valid_resp.user_id)
122143
return valid_resp
123144

@@ -206,6 +227,7 @@ async def request(self, route: Route) -> RawResponse | str | None:
206227
"last_validated": datetime.datetime.now().isoformat(),
207228
}
208229

230+
self._dispatch_event(old["user_id"], refresh)
209231
route.update_headers({"Authorization": f"Bearer {refresh.access_token}"})
210232
return await self.request(route)
211233

@@ -247,6 +269,8 @@ async def _refresh_token(self, user_id: str, refresh: str) -> None:
247269
"last_validated": datetime.datetime.now().isoformat(),
248270
}
249271

272+
self._dispatch_event(user_id, resp)
273+
250274
async def _revalidate_all(self) -> None:
251275
logger.debug("Attempting to revalidate all tokens that have passed the timeout on %s.", self.__class__.__qualname__)
252276

twitchio/client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ def __init__(
137137
redirect_uri=redirect_uri,
138138
scopes=scopes,
139139
session=session,
140+
client=self,
140141
)
141142
adapter: BaseAdapter | type[BaseAdapter] = options.get("adapter", AiohttpAdapter)
142143
if isinstance(adapter, BaseAdapter):

twitchio/events.pyi

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,17 @@ if TYPE_CHECKING:
55

66
from .authentication import UserTokenPayload
77
from .models.eventsub_ import SubscriptionRevoked
8+
from .payloads import TokenRefreshedPayload
9+
10+
async def event_token_refreshed(payload: TokenRefreshedPayload) -> None:
11+
"""Event dispatched when a token managed by the :class:`~twitchio.Client` is successfully refreshed.
12+
13+
You can use this event to update the stored token locally.
14+
15+
Parameters
16+
----------
17+
payload: TokenRefreshedPayload
18+
"""
819

920
async def event_oauth_authorized(payload: UserTokenPayload) -> None:
1021
"""Event dispatched when a user authorizes your Client-ID via Twitch OAuth on a built-in web adapter.

twitchio/payloads.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,13 @@
3232
if TYPE_CHECKING:
3333
from collections.abc import Callable, Coroutine
3434

35+
from .authentication import Scopes
3536
from .eventsub.enums import SubscriptionType
3637
from .types_.eventsub import Condition, _SubscriptionData
38+
from .types_.tokens import _TokenRefreshedPayload
3739

3840

39-
__all__ = ("EventErrorPayload", "WebsocketSubscriptionData")
41+
__all__ = ("EventErrorPayload", "TokenRefreshedPayload", "WebsocketSubscriptionData")
4042

4143

4244
class EventErrorPayload:
@@ -99,3 +101,37 @@ def __init__(self, data: _SubscriptionData) -> None:
99101
self.transport: TransportMethod = TransportMethod.WEBSOCKET
100102
self.type: SubscriptionType = data["type"]
101103
self.version: str = data["version"]
104+
105+
106+
class TokenRefreshedPayload:
107+
"""Payload received in the :func:`~twitchio.event_token_refreshed` event when a token managed by TwitchIO is successfully
108+
refreshed on the :class:`~twitchio.Client`.
109+
110+
Attributes
111+
----------
112+
user_id: str
113+
The User ID associated with the refreshed token.
114+
refresh_token: str
115+
The new refresh token returned by Twitch.
116+
token: str
117+
The updated token after refreshing returned by Twitch.
118+
scopes: :class:`~twitchio.Scopes`
119+
The scopes associated with the token.
120+
expires_in: int
121+
The time the new token expires as an :class:`int` of seconds.
122+
"""
123+
124+
__slots__ = (
125+
"expires_in",
126+
"refresh_token",
127+
"scopes",
128+
"token",
129+
"user_id",
130+
)
131+
132+
def __init__(self, data: _TokenRefreshedPayload) -> None:
133+
self.user_id: str = data["user_id"]
134+
self.refresh_token: str = data["refresh_token"]
135+
self.token: str = data["token"]
136+
self.scopes: Scopes = data["scopes"]
137+
self.expires_in: int = data["expires_in"]

twitchio/types_/tokens.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,16 @@
2222
SOFTWARE.
2323
"""
2424

25-
from typing import TypeAlias, TypedDict
25+
from __future__ import annotations
2626

27+
from typing import TYPE_CHECKING, TypeAlias, TypedDict
2728

28-
__all__ = ("TokenMapping", "TokenMappingData")
29+
30+
if TYPE_CHECKING:
31+
from ..authentication import Scopes
32+
33+
34+
__all__ = ("TokenMapping", "TokenMappingData", "_TokenRefreshedPayload")
2935

3036

3137
class TokenMappingData(TypedDict):
@@ -35,4 +41,12 @@ class TokenMappingData(TypedDict):
3541
last_validated: str
3642

3743

44+
class _TokenRefreshedPayload(TypedDict):
45+
user_id: str
46+
refresh_token: str
47+
token: str
48+
scopes: Scopes
49+
expires_in: int
50+
51+
3852
TokenMapping: TypeAlias = dict[str, TokenMappingData]

0 commit comments

Comments
 (0)