44import hashlib
55import logging
66from enum import Enum
7- from typing import Dict , TYPE_CHECKING , Optional , Type , Union , Tuple , List
7+ from typing import Dict , TYPE_CHECKING , Optional , Type , Union , Tuple , List , overload
88from typing_extensions import Literal
99
1010from aiohttp import web
1313
1414if TYPE_CHECKING :
1515 from .server import EventSubClient
16+ from .websocket import EventSubWSClient
1617
1718try :
1819 import ujson as json
@@ -36,7 +37,7 @@ def __init__(self, **kwargs):
3637
3738
3839class Subscription :
39- __slots__ = "id" , "status" , "type" , "version" , "cost" , "condition" , "transport" , "created_at"
40+ __slots__ = "id" , "status" , "type" , "version" , "cost" , "condition" , "transport" , "transport_method" , " created_at"
4041
4142 def __init__ (self , data : dict ):
4243 self .id : str = data ["id" ]
@@ -47,8 +48,14 @@ def __init__(self, data: dict):
4748 self .condition : Dict [str , str ] = data ["condition" ]
4849 self .created_at = _parse_datetime (data ["created_at" ])
4950 self .transport = EmptyObject ()
50- self .transport .method : str = data ["transport" ]["method" ] # noqa
51- self .transport .callback : str = data ["transport" ]["callback" ] # noqa
51+ self .transport_method : TransportType = getattr (TransportType , data ["transport" ]["method" ])
52+ self .transport .method : str = data ["transport" ]["method" ] # type: ignore
53+
54+ if self .transport_method is TransportType .webhook :
55+ self .transport .callback : str = data ["transport" ]["callback" ] # type: ignore
56+ else :
57+ self .transport .callback : str = "" # type: ignore # compatibility
58+ self .transport .session_id : str = data ["transport" ]["session_id" ] # type: ignore
5259
5360
5461class Headers :
@@ -82,6 +89,42 @@ def __init__(self, request: web.Request):
8289 self ._raw_timestamp = request .headers ["Twitch-Eventsub-Message-Timestamp" ]
8390
8491
92+ class WebsocketHeaders :
93+ """
94+ The headers of the inbound Websocket EventSub message
95+
96+ ..warning:
97+
98+ This is a BETA feature
99+
100+ Attributes
101+ -----------
102+ message_id: :class:`str`
103+ The unique ID of the message
104+ message_type: :class:`str`
105+ The type of the message coming through
106+ message_retry: :class:`int`
107+ Kept for compatibility with :class:`Headers`
108+ signature: :class:`str`
109+ Kept for compatibility with :class:`Headers`
110+ subscription_type: :class:`str`
111+ The type of the subscription on the inbound message
112+ subscription_version: :class:`str`
113+ The version of the subscription.
114+ timestamp: :class:`datetime.datetime`
115+ The timestamp the message was sent at
116+ """
117+ def __init__ (self , frame : dict ):
118+ meta = frame ["metadata" ]
119+ self .message_id : str = meta ["message_id" ]
120+ self .timestamp = _parse_datetime (meta ["message_timestamp" ])
121+ self .message_type : Literal ["notification" , "revocation" , "reconnect" , "session_keepalive" ] = meta ["message_type" ]
122+ self .message_retry : int = 0 # don't make breaking changes with the Header class
123+ self .signature : str = ""
124+ self .subscription_type : str = frame ["payload" ]["subscription" ]["type" ]
125+ self .subscription_version : str = frame ["payload" ]["subscription" ]["version" ]
126+
127+
85128class BaseEvent :
86129 """
87130 The base of all the event classes
@@ -94,21 +137,47 @@ class BaseEvent:
94137 The headers received with the message
95138 """
96139
97- __slots__ = "_client" , "_raw_data" , "subscription" , "headers"
140+ __slots__ = ("_client" , "_raw_data" , "subscription" , "headers" )
141+
142+ @overload
143+ def __init__ (self , client : EventSubClient , _data : str , request : web .Request ):
144+ ...
98145
99- def __init__ (self , client : EventSubClient , data : str , request : web .Request ):
146+ @overload
147+ def __init__ (self , client : EventSubWSClient , _data : dict , request : None ):
148+ ...
149+
150+ def __init__ (self , client : Union [EventSubClient , EventSubWSClient ], _data : Union [str , dict ], request : Optional [web .Request ]):
100151 self ._client = client
101- self ._raw_data = data
102- _data : dict = _loads (data )
103- self .subscription = Subscription (_data ["subscription" ])
104- self .headers = Headers (request )
105- self .setup (_data )
152+ self ._raw_data = _data
153+
154+ if isinstance (_data , str ):
155+ data : dict = _loads (_data )
156+ else :
157+ data = _data
158+
159+ self .headers : Union [Headers , WebsocketHeaders ]
160+ self .subscription : Subscription
161+
162+ if request :
163+ data : dict = _loads (_data )
164+ self .headers = Headers (request )
165+ self .subscription = Subscription (data ["subscription" ])
166+ self .setup (data )
167+ else :
168+ self .headers = WebsocketHeaders (data )
169+ self .subscription = Subscription (data ["payload" ]["subscription" ])
170+ self .setup (data ["payload" ])
171+
106172
107173 def setup (self , data : dict ):
108174 pass
109175
110176 def verify (self ):
111- hmac_message = (self .headers .message_id + self .headers ._raw_timestamp + self ._raw_data ).encode ("utf-8" )
177+ """
178+ Only used in webhook transport types. Verifies the message is valid
179+ """
180+ hmac_message = (self .headers .message_id + self .headers ._raw_timestamp + self ._raw_data ).encode ("utf-8" ) # type: ignore
112181 secret = self ._client .secret .encode ("utf-8" )
113182 digest = hmac .new (secret , msg = hmac_message , digestmod = hashlib .sha256 ).hexdigest ()
114183
@@ -127,6 +196,9 @@ class ChallengeEvent(BaseEvent):
127196 """
128197 A challenge event.
129198
199+ .. note::
200+ These are only dispatched when using :class:`~twitchio.ext.eventsub.EventSubClient`
201+
130202 Attributes
131203 -----------
132204 challenge: :class`str`
@@ -139,7 +211,7 @@ def setup(self, data: dict):
139211 self .challenge : str = data ["challenge" ]
140212
141213 def verify (self ):
142- hmac_message = (self .headers .message_id + self .headers ._raw_timestamp + self ._raw_data ).encode ("utf-8" )
214+ hmac_message = (self .headers .message_id + self .headers ._raw_timestamp + self ._raw_data ).encode ("utf-8" ) # type: ignore
143215 secret = self ._client .secret .encode ("utf-8" )
144216 digest = hmac .new (secret , msg = hmac_message , digestmod = hashlib .sha256 ).hexdigest ()
145217
@@ -150,6 +222,39 @@ def verify(self):
150222 return web .Response (status = 200 , text = self .challenge )
151223
152224
225+ class ReconnectEvent (BaseEvent ):
226+ """
227+ A reconnect event. Called by twitch when the websocket needs to be disconnected for maintenance or other reasons
228+
229+ .. note::
230+ These are only dispatched when using :class:`~twitchio.ext.eventsub.EventSubWSClient
231+
232+ Attributes
233+ -----------
234+ reconnect_url: :class:`str`
235+ The URL to reconnect to
236+ connected_at: :class:`~datetime.datetime`
237+ When the original websocket connected
238+ """
239+
240+ __slots__ = ("reconnect_url" , "connected_at" )
241+
242+ def setup (self , data : dict ):
243+ self .reconnect_url : str = data ["session" ]["reconnect_url" ]
244+ self .connected_at : datetime .datetime = _parse_datetime (data ["session" ]["connected_at" ])
245+
246+
247+ class KeepAliveEvent (BaseEvent ):
248+ """
249+ A keep-alive event. Called by twitch when no message has been sent for more than ``keepalive_timeout``
250+
251+ .. note::
252+ These are only dispatched when using :class:`~twitchio.ext.eventsub.EventSubWSClient
253+
254+ """
255+ pass
256+
257+
153258class NotificationEvent (BaseEvent ):
154259 """
155260 A notification event
@@ -1410,3 +1515,7 @@ class _SubscriptionTypes(metaclass=_SubTypesMeta):
14101515
14111516
14121517SubscriptionTypes = _SubscriptionTypes ()
1518+
1519+ class TransportType (Enum ):
1520+ webhook = "webhook"
1521+ websocket = "websocket"
0 commit comments