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,33 +89,104 @@ 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+ Attributes
97+ -----------
98+ message_id: :class:`str`
99+ The unique ID of the message
100+ message_type: :class:`str`
101+ The type of the message coming through
102+ message_retry: :class:`int`
103+ Kept for compatibility with :class:`Headers`
104+ signature: :class:`str`
105+ Kept for compatibility with :class:`Headers`
106+ subscription_type: :class:`str`
107+ The type of the subscription on the inbound message
108+ subscription_version: :class:`str`
109+ The version of the subscription.
110+ timestamp: :class:`datetime.datetime`
111+ The timestamp the message was sent at
112+ """
113+
114+ def __init__ (self , frame : dict ):
115+ meta = frame ["metadata" ]
116+ self .message_id : str = meta ["message_id" ]
117+ self .timestamp = _parse_datetime (meta ["message_timestamp" ])
118+ self .message_type : Literal ["notification" , "revocation" , "reconnect" , "session_keepalive" ] = meta [
119+ "message_type"
120+ ]
121+ self .message_retry : int = 0 # don't make breaking changes with the Header class
122+ self .signature : str = ""
123+ self .subscription_type : Optional [str ]
124+ self .subscription_version : Optional [str ]
125+ if frame ["payload" ]:
126+ self .subscription_type = frame ["payload" ]["subscription" ]["type" ]
127+ self .subscription_version = frame ["payload" ]["subscription" ]["version" ]
128+ else :
129+ self .subscription_type = None
130+ self .subscription_version = None
131+
132+
85133class BaseEvent :
86134 """
87135 The base of all the event classes
88136
89137 Attributes
90138 -----------
91- subscription: :class:`Subscription`
92- The subscription attached to the message
139+ subscription: Optional[ :class:`Subscription`]
140+ The subscription attached to the message. This is only optional when using the websocket eventsub transport
93141 headers: :class`Headers`
94142 The headers received with the message
95143 """
96144
97- __slots__ = "_client" , "_raw_data" , "subscription" , "headers"
145+ __slots__ = ("_client" , "_raw_data" , "subscription" , "headers" )
146+
147+ @overload
148+ def __init__ (self , client : EventSubClient , _data : str , request : web .Request ):
149+ ...
98150
99- def __init__ (self , client : EventSubClient , data : str , request : web .Request ):
151+ @overload
152+ def __init__ (self , client : EventSubWSClient , _data : dict , request : None ):
153+ ...
154+
155+ def __init__ (
156+ self , client : Union [EventSubClient , EventSubWSClient ], _data : Union [str , dict ], request : Optional [web .Request ]
157+ ):
100158 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 )
159+ self ._raw_data = _data
160+
161+ if isinstance (_data , str ):
162+ data : dict = _loads (_data )
163+ else :
164+ data = _data
165+
166+ self .headers : Union [Headers , WebsocketHeaders ]
167+ self .subscription : Optional [Subscription ]
168+
169+ if request :
170+ data : dict = _loads (_data )
171+ self .headers = Headers (request )
172+ self .subscription = Subscription (data ["subscription" ])
173+ self .setup (data )
174+ else :
175+ self .headers = WebsocketHeaders (data )
176+ if data ["payload" ]:
177+ self .subscription = Subscription (data ["payload" ]["subscription" ])
178+ else :
179+ self .subscription = None
180+ self .setup (data ["payload" ])
106181
107182 def setup (self , data : dict ):
108183 pass
109184
110185 def verify (self ):
111- hmac_message = (self .headers .message_id + self .headers ._raw_timestamp + self ._raw_data ).encode ("utf-8" )
186+ """
187+ Only used in webhook transport types. Verifies the message is valid
188+ """
189+ hmac_message = (self .headers .message_id + self .headers ._raw_timestamp + self ._raw_data ).encode ("utf-8" ) # type: ignore
112190 secret = self ._client .secret .encode ("utf-8" )
113191 digest = hmac .new (secret , msg = hmac_message , digestmod = hashlib .sha256 ).hexdigest ()
114192
@@ -127,6 +205,9 @@ class ChallengeEvent(BaseEvent):
127205 """
128206 A challenge event.
129207
208+ .. note::
209+ These are only dispatched when using :class:`~twitchio.ext.eventsub.EventSubClient`
210+
130211 Attributes
131212 -----------
132213 challenge: :class`str`
@@ -139,7 +220,7 @@ def setup(self, data: dict):
139220 self .challenge : str = data ["challenge" ]
140221
141222 def verify (self ):
142- hmac_message = (self .headers .message_id + self .headers ._raw_timestamp + self ._raw_data ).encode ("utf-8" )
223+ hmac_message = (self .headers .message_id + self .headers ._raw_timestamp + self ._raw_data ).encode ("utf-8" ) # type: ignore
143224 secret = self ._client .secret .encode ("utf-8" )
144225 digest = hmac .new (secret , msg = hmac_message , digestmod = hashlib .sha256 ).hexdigest ()
145226
@@ -150,6 +231,40 @@ def verify(self):
150231 return web .Response (status = 200 , text = self .challenge )
151232
152233
234+ class ReconnectEvent (BaseEvent ):
235+ """
236+ A reconnect event. Called by twitch when the websocket needs to be disconnected for maintenance or other reasons
237+
238+ .. note::
239+ These are only dispatched when using :class:`~twitchio.ext.eventsub.EventSubWSClient`
240+
241+ Attributes
242+ -----------
243+ reconnect_url: :class:`str`
244+ The URL to reconnect to
245+ connected_at: :class:`datetime.datetime`
246+ When the original websocket connected
247+ """
248+
249+ __slots__ = ("reconnect_url" , "connected_at" )
250+
251+ def setup (self , data : dict ):
252+ self .reconnect_url : str = data ["session" ]["reconnect_url" ]
253+ self .connected_at : datetime .datetime = _parse_datetime (data ["session" ]["connected_at" ])
254+
255+
256+ class KeepAliveEvent (BaseEvent ):
257+ """
258+ A keep-alive event. Called by twitch when no message has been sent for more than ``keepalive_timeout``
259+
260+ .. note::
261+ These are only dispatched when using :class:`~twitchio.ext.eventsub.EventSubWSClient`
262+
263+ """
264+
265+ pass
266+
267+
153268class NotificationEvent (BaseEvent ):
154269 """
155270 A notification event
@@ -1536,3 +1651,8 @@ class _SubscriptionTypes(metaclass=_SubTypesMeta):
15361651
15371652
15381653SubscriptionTypes = _SubscriptionTypes ()
1654+
1655+
1656+ class TransportType (Enum ):
1657+ webhook = "webhook"
1658+ websocket = "websocket"
0 commit comments