Skip to content

Commit 52298ee

Browse files
committed
2 parents 7b41c63 + f30b2ff commit 52298ee

File tree

15 files changed

+256
-41
lines changed

15 files changed

+256
-41
lines changed

docs/references/events.rst

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ Events Reference
4444
- :meth:`~eventsub.AdBreakBeginSubscription`
4545
- :func:`~twitchio.event_ad_break()`
4646
- :class:`~models.eventsub_.ChannelAdBreakBegin`
47+
* - Channel Bits Use
48+
- :meth:`~eventsub.ChannelBitsUseSubscription`
49+
- :func:`~twitchio.event_bits_use()`
50+
- :class:`~models.eventsub_.ChannelBitsUse`
4751
* - Channel Chat Clear
4852
- :meth:`~eventsub.ChatClearSubscription`
4953
- :func:`~twitchio.event_chat_clear()`
@@ -794,6 +798,18 @@ Chat / Messages
794798

795799
:param twitchio.Whisper payload: The EventSub payload for this event.
796800

801+
.. py:function:: event_bits_use(payload: twitchio.ChannelBitsUse) -> None
802+
:async:
803+
804+
Event dispatched whenever Bits are used on a channel.
805+
806+
Corresponds to the Twitch EventSub subscription :es-docs:`Channel Bits Use <channelbitsuse>`.
807+
808+
You must subscribe to EventSub with :class:`~twitchio.eventsub.ChannelBitsUseSubscription` for each required user
809+
to receive this event.
810+
811+
:param twitchio.ChannelBitsUse payload: The EventSub payload for this event.
812+
797813
Goals
798814
-----
799815

docs/references/eventsub_models.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,11 @@ Eventsub
4848
.. autoclass:: twitchio.ChannelAdBreakBegin()
4949
:members:
5050

51+
.. attributetable:: twitchio.ChannelBitsUse
52+
53+
.. autoclass:: twitchio.ChannelBitsUse()
54+
:members:
55+
5156
.. attributetable:: twitchio.ChannelChatClear
5257

5358
.. autoclass:: twitchio.ChannelChatClear()
@@ -476,6 +481,16 @@ Eventsub
476481
.. autoclass:: twitchio.HypeTrainEnd()
477482
:members:
478483

484+
.. attributetable:: twitchio.PowerUp
485+
486+
.. autoclass:: twitchio.PowerUp()
487+
:members:
488+
489+
.. attributetable:: twitchio.PowerUpEmote
490+
491+
.. autoclass:: twitchio.PowerUpEmote()
492+
:members:
493+
479494
.. attributetable:: twitchio.ShieldModeBegin
480495

481496
.. autoclass:: twitchio.ShieldModeBegin()

docs/references/eventsub_subscriptions.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ Eventsub Subscriptions
3434
.. autoclass:: AutomodTermsUpdateSubscription
3535
:members:
3636

37+
.. attributetable:: ChannelBitsUseSubscription
38+
39+
.. autoclass:: ChannelBitsUseSubscription
40+
:members:
41+
3742
.. attributetable:: ChannelUpdateSubscription
3843

3944
.. autoclass:: ChannelUpdateSubscription

twitchio/client.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,7 @@ async def login(self, *, token: str | None = None, load_tokens: bool = True, sav
334334
if self._bot_id:
335335
logger.debug("Fetching Clients self user for %r", self)
336336
partial = PartialUser(id=self._bot_id, http=self._http)
337-
self._user = partial if not self._fetch_self else await partial.user()
337+
self._user = await partial.user() if self._fetch_self else partial
338338

339339
await self.setup_hook()
340340

@@ -2311,6 +2311,8 @@ async def fetch_eventsub_subscriptions(
23112311
.. note::
23122312
type, status and user_id are mutually exclusive and only one can be passed, otherwise ValueError will be raised.
23132313
2314+
This endpoint returns disabled WebSocket subscriptions for a minimum of 1 minute as compared to webhooks which returns disabled subscriptions for a minimum of 10 days.
2315+
23142316
Parameters
23152317
-----------
23162318
token_for: str | PartialUser | None

twitchio/events.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ async def event_chat_clear_user(payload: twitchio.ChannelChatClearUserMessages)
7676
async def event_chat_settings_update(payload: twitchio.ChatSettingsUpdate) -> None: ...
7777
async def event_chat_user_message_hold(payload: twitchio.ChatUserMessageHold) -> None: ...
7878
async def event_chat_user_message_update(payload: twitchio.ChatUserMessageUpdate) -> None: ...
79+
async def event_bits_use(payload: twitchio.ChannelBitsUse) -> None: ...
7980

8081
# Shared Chat
8182
async def event_shared_chat_begin(payload: twitchio.SharedChatSessionBegin) -> None: ...

twitchio/eventsub/enums.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ class SubscriptionType(enum.Enum):
9999
ChannelUpdate: Literal["channel.update"]
100100
ChannelFollow: Literal["channel.follow"]
101101
ChannelAdBreakBegin: Literal["channel.ad_break.begin"]
102+
ChannelBitsUseSubscription: Literal["channel.bits.use"]
102103
ChannelChatClear: Literal["channel.chat.clear"]
103104
ChannelChatClearUserMessages: Literal["channel.chat.clear_user_messages"]
104105
ChannelChatMessage: Literal["channel.chat.message"]
@@ -172,6 +173,7 @@ class SubscriptionType(enum.Enum):
172173
AutomodMessageUpdate = "automod.message.update"
173174
AutomodSettingsUpdate = "automod.settings.update"
174175
AutomodTermsUpdate = "automod.terms.update"
176+
ChannelBitsUse = "channel.bits.use"
175177
ChannelUpdate = "channel.update"
176178
ChannelFollow = "channel.follow"
177179
ChannelAdBreakBegin = "channel.ad_break.begin"

twitchio/eventsub/subscriptions.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
"AutomodSettingsUpdateSubscription",
4444
"AutomodTermsUpdateSubscription",
4545
"ChannelBanSubscription",
46+
"ChannelBitsUseSubscription",
4647
"ChannelCheerSubscription",
4748
"ChannelFollowSubscription",
4849
"ChannelModerateSubscription",
@@ -115,6 +116,7 @@
115116
# Short names: Only map names that require shortening...
116117
_SUB_MAPPING: dict[str, str] = {
117118
"channel.ad_break.begin": "ad_break",
119+
"channel.bits.use": "bits_use",
118120
"channel.chat.clear_user_messages": "chat_clear_user",
119121
"channel.chat.message": "message", # Sub events?
120122
"channel.chat.message_delete": "message_delete",
@@ -404,6 +406,51 @@ def condition(self) -> Condition:
404406
return {"broadcaster_user_id": self.broadcaster_user_id, "moderator_user_id": self.moderator_user_id}
405407

406408

409+
class ChannelBitsUseSubscription(SubscriptionPayload):
410+
"""The ``channel.bits.use`` subscription type sends a notification whenever Bits are used on a channel.
411+
412+
This event is designed to be an all-purpose event for when Bits are used in a channel and might be updated in the future as more Twitch features use Bits.
413+
414+
Currently, this event will be sent when a user:
415+
416+
- Cheers in a channel
417+
- Uses a Power-up
418+
- Will not emit when a streamer uses a Power-up for free in their own channel.
419+
420+
.. important::
421+
Requires a user access token that includes the ``bits:read`` scope. This must be the broadcaster's token.
422+
423+
Bits transactions via Twitch Extensions are not included in this subscription type.
424+
425+
One attribute ``.condition`` can be accessed from this class, which returns a mapping of the subscription
426+
parameters provided.
427+
428+
Parameters
429+
----------
430+
broadcaster_user_id: str | PartialUser
431+
The ID, or PartialUser, of the broadcaster to subscribe to.
432+
433+
Raises
434+
------
435+
ValueError
436+
The parameters "broadcaster_user_id" must be passed.
437+
"""
438+
439+
type: ClassVar[Literal["channel.bits.use"]] = "channel.bits.use"
440+
version: ClassVar[Literal["1"]] = "1"
441+
442+
@handle_user_ids()
443+
def __init__(self, **condition: Unpack[Condition]) -> None:
444+
self.broadcaster_user_id: str = condition.get("broadcaster_user_id", "")
445+
446+
if not self.broadcaster_user_id:
447+
raise ValueError('The parameter "broadcaster_user_id" must be passed.')
448+
449+
@property
450+
def condition(self) -> Condition:
451+
return {"broadcaster_user_id": self.broadcaster_user_id}
452+
453+
407454
class ChannelUpdateSubscription(SubscriptionPayload):
408455
"""The ``channel.update`` subscription type sends notifications when a broadcaster updates the category, title, content classification labels, or broadcast language for their channel.
409456

twitchio/eventsub/websockets.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,6 @@
3434
from ..backoff import Backoff
3535
from ..exceptions import HTTPException, WebsocketConnectionException
3636
from ..models.eventsub_ import SubscriptionRevoked, create_event_instance
37-
from ..types_.conduits import (
38-
MessageTypes,
39-
MetaData,
40-
NotificationMessage,
41-
ReconnectMessage,
42-
RevocationMessage,
43-
WebsocketMessages,
44-
WelcomeMessage,
45-
WelcomePayload,
46-
)
4737
from ..utils import (
4838
MISSING,
4939
_from_json, # type: ignore
@@ -54,7 +44,17 @@
5444
if TYPE_CHECKING:
5545
from ..authentication.tokens import ManagedHTTPClient
5646
from ..client import Client
57-
from ..types_.conduits import Condition
47+
from ..types_.conduits import (
48+
Condition,
49+
MessageTypes,
50+
MetaData,
51+
NotificationMessage,
52+
ReconnectMessage,
53+
RevocationMessage,
54+
WebsocketMessages,
55+
WelcomeMessage,
56+
WelcomePayload,
57+
)
5858
from ..types_.eventsub import SubscriptionResponse, _SubscriptionData
5959

6060

@@ -327,7 +327,7 @@ async def _listen(self) -> None:
327327
self._last_keepalive = datetime.datetime.now()
328328

329329
try:
330-
data: WebsocketMessages = cast(WebsocketMessages, _from_json(message.data))
330+
data: WebsocketMessages = cast("WebsocketMessages", _from_json(message.data))
331331
except Exception:
332332
logger.warning("Unable to parse JSON in eventsub websocket: <%s>", self)
333333
continue
@@ -336,13 +336,13 @@ async def _listen(self) -> None:
336336
message_type: MessageTypes = metadata["message_type"]
337337

338338
if message_type == "session_welcome":
339-
welcome_data: WelcomeMessage = cast(WelcomeMessage, data)
339+
welcome_data: WelcomeMessage = cast("WelcomeMessage", data)
340340

341341
await self._process_welcome(welcome_data)
342342

343343
elif message_type == "session_reconnect":
344344
logger.debug('Received "session_reconnect" message from eventsub websocket: <%s>', self)
345-
reconnect_data: ReconnectMessage = cast(ReconnectMessage, data)
345+
reconnect_data: ReconnectMessage = cast("ReconnectMessage", data)
346346

347347
await self._process_reconnect(reconnect_data)
348348

@@ -352,12 +352,12 @@ async def _listen(self) -> None:
352352
elif message_type == "revocation":
353353
logger.debug('Received "revocation" message from eventsub websocket: <%s>', self)
354354

355-
revocation_data: RevocationMessage = cast(RevocationMessage, data)
355+
revocation_data: RevocationMessage = cast("RevocationMessage", data)
356356
await self._process_revocation(revocation_data)
357357

358358
elif message_type == "notification":
359359
logger.debug('Received "notification" message from eventsub websocket: <%s>. %s', self, data)
360-
notification_data: NotificationMessage = cast(NotificationMessage, data)
360+
notification_data: NotificationMessage = cast("NotificationMessage", data)
361361

362362
try:
363363
await self._process_notification(notification_data)

twitchio/ext/commands/core.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ def __str__(self) -> str:
168168

169169
async def __call__(self, context: Context) -> Any:
170170
callback = self._callback(self._injected, context) if self._injected else self._callback(context) # type: ignore
171-
return await callback
171+
return await callback # type: ignore will fix later
172172

173173
@property
174174
def component(self) -> Component_T | None:
@@ -197,7 +197,7 @@ def relative_name(self) -> str:
197197
198198
If this command has no parent, this simply returns the name.
199199
"""
200-
return self._name if not self._parent else f"{self._parent._name} {self._name}"
200+
return f"{self._parent._name} {self._name}" if self._parent else self._name
201201

202202
@property
203203
def full_parent_name(self) -> str:

twitchio/models/ads.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,13 @@
2424

2525
from __future__ import annotations
2626

27+
import datetime
2728
from typing import TYPE_CHECKING
2829

2930
from twitchio.utils import parse_timestamp
3031

3132

3233
if TYPE_CHECKING:
33-
import datetime
34-
3534
from twitchio.types_.responses import AdScheduleResponseData, SnoozeNextAdResponseData, StartCommercialResponseData
3635

3736

@@ -70,7 +69,7 @@ class AdSchedule:
7069
-----------
7170
snooze_count: int
7271
The number of snoozes available for the broadcaster.
73-
snooze_refresh_at: datetime.datetime
72+
snooze_refresh_at: datetime.datetime | None
7473
The UTC datetime when the broadcaster will gain an additional snooze.
7574
duration: int
7675
The length in seconds of the scheduled upcoming ad break.
@@ -86,10 +85,12 @@ class AdSchedule:
8685

8786
def __init__(self, data: AdScheduleResponseData) -> None:
8887
self.snooze_count: int = int(data["snooze_count"])
89-
self.snooze_refresh_at: datetime.datetime = parse_timestamp(data["snooze_refresh_at"])
88+
self.snooze_refresh_at: datetime.datetime | None = (
89+
_parse_timestamp(data["snooze_refresh_at"]) if data["snooze_refresh_at"] else None
90+
)
9091
self.duration: int = int(data["duration"])
91-
self.next_ad_at: datetime.datetime | None = parse_timestamp(data["next_ad_at"]) if data["next_ad_at"] else None
92-
self.last_ad_at: datetime.datetime | None = parse_timestamp(data["last_ad_at"]) if data["last_ad_at"] else None
92+
self.next_ad_at: datetime.datetime | None = _parse_timestamp(data["next_ad_at"]) if data["next_ad_at"] else None
93+
self.last_ad_at: datetime.datetime | None = _parse_timestamp(data["last_ad_at"]) if data["last_ad_at"] else None
9394
self.preroll_free_time: int = int(data["preroll_free_time"])
9495

9596
def __repr__(self) -> str:
@@ -104,7 +105,7 @@ class SnoozeAd:
104105
-----------
105106
snooze_count: int
106107
The number of snoozes available for the broadcaster.
107-
snooze_refresh_at: datetime.datetime
108+
snooze_refresh_at: datetime.datetime | None
108109
The UTC datetime when the broadcaster will gain an additional snooze.
109110
next_ad_at: datetime.datetime | None
110111
The UTC datetime of the broadcaster's next scheduled ad. None if channel has no ad scheduled.
@@ -114,8 +115,19 @@ class SnoozeAd:
114115

115116
def __init__(self, data: SnoozeNextAdResponseData) -> None:
116117
self.snooze_count: int = int(data["snooze_count"])
117-
self.snooze_refresh_at: datetime.datetime = parse_timestamp(data["snooze_refresh_at"])
118-
self.next_ad_at: datetime.datetime | None = parse_timestamp(data["next_ad_at"]) if data["next_ad_at"] else None
118+
self.snooze_refresh_at: datetime.datetime | None = (
119+
_parse_timestamp(data["snooze_refresh_at"]) if data["snooze_refresh_at"] else None
120+
)
121+
self.next_ad_at: datetime.datetime | None = _parse_timestamp(data["next_ad_at"]) if data["next_ad_at"] else None
119122

120123
def __repr__(self) -> str:
121124
return f"<SnoozeAd snooze_count={self.snooze_count} snooze_refresh_at={self.snooze_refresh_at} next_ad_at={self.next_ad_at}>"
125+
126+
127+
def _parse_timestamp(timestamp: str | int) -> datetime.datetime:
128+
"""Helper function for Ads due to incorrect Twitch documention and a known issue with the return format.
129+
This may be incorporated into the main `parse_timestamp` utility function in the future.
130+
"""
131+
if isinstance(timestamp, str):
132+
return parse_timestamp(timestamp)
133+
return datetime.datetime.fromtimestamp(timestamp, tz=datetime.UTC)

0 commit comments

Comments
 (0)