Skip to content

Commit bd81427

Browse files
committed
Merge remote-tracking branch 'origin/master' into fix/routines
Updater with latest changes from master.
2 parents c79c225 + 8a2a839 commit bd81427

File tree

12 files changed

+380
-52
lines changed

12 files changed

+380
-52
lines changed

.github/PULL_REQUEST_TEMPLATE.md

Lines changed: 0 additions & 18 deletions
This file was deleted.

.github/workflows/signoff.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: validate-signoff
2+
on:
3+
pull_request:
4+
types:
5+
- opened
6+
- edited
7+
8+
jobs:
9+
validate:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: PR Description Check
13+
uses: pythonistaguild/[email protected]
14+
with:
15+
content: "[x] I have read and agree to the [Developer Certificate of Origin]"

README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ Thank you to all those who contribute and help TwitchIO grow.
104104

105105
Special thanks to:
106106

107-
`SnowyLuma <https://github.com/SnowyLuma>`_
107+
`LostLuma (Lilly) <https://github.com/LostLuma>`_
108108

109109
`Harmon <https://github.com/Harmon758>`_
110110

docs/changelog.rst

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,42 @@
11
:orphan:
22

3+
2.9.2
4+
=======
5+
- TwitchIO
6+
- Changes:
7+
- :func:`PartialUser.fetch_moderated_channels <twitchio.PartialUser.fetch_moderated_channels>` returns "broadcaster_login" api field instead of "broadcaster_name"
8+
9+
- Bug fixes
10+
- fix: :func:`PartialUser.fetch_moderated_channels <twitchio.PartialUser.fetch_moderated_channels>` used "user_" prefix from payload, now uses "broadcaster_" instead
11+
12+
13+
2.9.1
14+
=======
15+
- ext.eventsub
16+
- Bug fixes
17+
- fix: Special-cased a restart when a specific known bad frame is received.
18+
19+
20+
2.9.0
21+
=======
22+
- TwitchIO
23+
- Additions
24+
- Added :class:`~twitchio.AdSchedule` and :class:`~twitchio.Emote`
25+
- Added the new ad-related methods for :class:`~twitchio.PartialUser`:
26+
- :func:`~twitchio.PartialUser.fetch_ad_schedule`
27+
- :func:`~twitchio.PartialUser.snooze_ad`
28+
- Added new method :func:`~twitchio.PartialUser.fetch_user_emotes` to :class:`~twitchio.PartialUser`
29+
- Added :func:`~twitchio.PartialUser.fetch_moderated_channels` to :class:`~twitchio.PartialUser`
30+
31+
- Bug fixes
32+
- Fixed ``event_token_expired`` not applying to the current request.
33+
34+
- ext.eventsub
35+
- Bug fixes
36+
- Fixed a crash where a Future could be None, causing unintentional errors.
37+
- Special-cased a restart when a specific known bad frame is received.
38+
39+
340
2.8.2
441
======
542
- ext.commands

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626

2727
on_rtd = os.environ.get("READTHEDOCS") == "True"
2828
project = "TwitchIO"
29-
copyright = "2023, TwitchIO"
29+
copyright = "2024, TwitchIO"
3030
author = "PythonistaGuild"
3131

3232
# The full version, including alpha/beta/rc tags

docs/exts/eventsub.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ This is a list of events dispatched by the eventsub ext.
150150

151151
Called when someone cheers on a channel you've subscribed to.
152152

153-
.. function:: event_eventsub_notification_raid(event: Channel)
153+
.. function:: event_eventsub_notification_raid(event: ChannelRaidData)
154154

155155
Called when someone raids a channel you've subscribed to.
156156

docs/reference.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@ ActiveExtension
1111
:members:
1212
:inherited-members:
1313

14+
AdSchedule
15+
------------
16+
.. attributetable:: AdSchedule
17+
18+
.. autoclass:: AdSchedule
19+
:members:
20+
1421
AutomodCheckMessage
1522
---------------------
1623
.. attributetable:: AutomodCheckMessage
@@ -213,6 +220,13 @@ CustomRewardRedemption
213220
:members:
214221
:inherited-members:
215222

223+
Emote
224+
------
225+
.. attributetable:: Emote
226+
227+
.. autoclass:: Emote
228+
:members:
229+
216230
Extension
217231
-----------
218232
.. attributetable:: Extension

twitchio/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
__title__ = "TwitchIO"
2828
__author__ = "TwitchIO, PythonistaGuild"
2929
__license__ = "MIT"
30-
__copyright__ = "Copyright 2017-2022 (c) TwitchIO"
31-
__version__ = "2.8.2"
30+
__copyright__ = "Copyright 2017-present (c) TwitchIO"
31+
__version__ = "2.9.1"
3232

3333
from .client import Client
3434
from .user import *

twitchio/ext/eventsub/websocket.py

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from twitchio import PartialUser, Unauthorized, HTTPException
1111

1212
if TYPE_CHECKING:
13+
from typing_extensions import Literal
1314
from twitchio import Client
1415

1516
logger = logging.getLogger("twitchio.ext.eventsub.ws")
@@ -32,7 +33,7 @@ def __init__(self, event_type: Tuple[str, int, Type[models.EventData]], conditio
3233
self.token = token
3334
self.subscription_id: Optional[str] = None
3435
self.cost: Optional[int] = None
35-
self.created: asyncio.Future[Tuple[bool, int]] | None = asyncio.Future()
36+
self.created: asyncio.Future[Tuple[Literal[False], int] | Tuple[Literal[True], None]] | None = asyncio.Future()
3637

3738

3839
_T = TypeVar("_T")
@@ -117,18 +118,25 @@ async def _subscribe(self, obj: _Subscription) -> dict | None:
117118
try:
118119
resp = await self._http.create_websocket_subscription(obj.event, obj.condition, self._session_id, obj.token)
119120
except HTTPException as e:
120-
assert obj.created
121-
obj.created.set_result((False, e.status)) # type: ignore
121+
if obj.created:
122+
obj.created.set_result((False, e.status))
123+
124+
else:
125+
logger.error(
126+
"An error (%s %s) occurred while attempting to resubscribe to an event on reconnect: %s",
127+
e.status,
128+
e.reason,
129+
e.message,
130+
)
131+
122132
return None
123133

124-
else:
125-
assert obj.created
126-
obj.created.set_result((True, None)) # type: ignore
134+
if obj.created:
135+
obj.created.set_result((True, None))
127136

128137
data = resp["data"][0]
129-
cost = data["cost"]
130138
self.remaining_slots = resp["max_total_cost"] - resp["total_cost"]
131-
obj.cost = cost
139+
obj.cost = data["cost"]
132140

133141
return data
134142

@@ -204,6 +212,11 @@ async def pump(self) -> None:
204212
except TypeError as e:
205213
logger.warning(f"Received bad frame: {e.args[0]}")
206214

215+
if "257" in e.args[0]: # websocket was closed, reconnect
216+
logger.info("Known bad frame, restarting connection")
217+
await self.connect()
218+
return
219+
207220
except Exception as e:
208221
logger.error("Exception in the pump function!", exc_info=e)
209222
raise

twitchio/http.py

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -121,22 +121,8 @@ async def request(self, route: Route, *, paginate=True, limit=100, full_body=Fal
121121
raise errors.NoClientID("A Client ID is required to use the Twitch API")
122122
headers = route.headers or {}
123123

124-
if force_app_token and "Authorization" not in headers:
125-
if not self.client_secret:
126-
raise errors.NoToken(
127-
"An app access token is required for this route, please provide a client id and client secret"
128-
)
129-
if self.app_token is None:
130-
await self._generate_login()
131-
headers["Authorization"] = f"Bearer {self.app_token}"
132-
elif not self.token and not self.client_secret and "Authorization" not in headers:
133-
raise errors.NoToken(
134-
"Authorization is required to use the Twitch API. Pass token and/or client_secret to the Client constructor"
135-
)
136-
if "Authorization" not in headers:
137-
if not self.token:
138-
await self._generate_login()
139-
headers["Authorization"] = f"Bearer {self.token}"
124+
await self._apply_auth(headers, force_app_token, False)
125+
140126
headers["Client-ID"] = self.client_id
141127

142128
if not self.session:
@@ -165,7 +151,7 @@ def get_limit():
165151
q = [("after", cursor), *q]
166152
q = [("first", get_limit()), *q]
167153
path = path.with_query(q)
168-
body, is_text = await self._request(route, path, headers)
154+
body, is_text = await self._request(route, path, headers, force_app_token=force_app_token)
169155
if is_text:
170156
return body
171157
if full_body:
@@ -182,7 +168,26 @@ def get_limit():
182168
is_finished = reached_limit() if limit is not None else True if paginate else True
183169
return data
184170

185-
async def _request(self, route, path, headers, utilize_bucket=True):
171+
async def _apply_auth(self, headers: dict, force_app_token: bool, force_apply: bool) -> None:
172+
if force_app_token and "Authorization" not in headers:
173+
if not self.client_secret:
174+
raise errors.NoToken(
175+
"An app access token is required for this route, please provide a client id and client secret"
176+
)
177+
if self.app_token is None:
178+
await self._generate_login()
179+
headers["Authorization"] = f"Bearer {self.app_token}"
180+
elif not self.token and not self.client_secret and "Authorization" not in headers:
181+
raise errors.NoToken(
182+
"Authorization is required to use the Twitch API. Pass token and/or client_secret to the Client constructor"
183+
)
184+
if "Authorization" not in headers or force_apply:
185+
if not self.token:
186+
await self._generate_login()
187+
188+
headers["Authorization"] = f"Bearer {self.token}"
189+
190+
async def _request(self, route, path, headers, utilize_bucket=True, force_app_token: bool = False):
186191
reason = None
187192

188193
for attempt in range(5):
@@ -224,6 +229,7 @@ async def _request(self, route, path, headers, utilize_bucket=True):
224229
if "Invalid OAuth token" in message_json.get("message", ""):
225230
try:
226231
await self._generate_login()
232+
await self._apply_auth(headers, force_app_token, True)
227233
continue
228234
except:
229235
raise errors.Unauthorized(
@@ -645,7 +651,6 @@ async def get_hype_train(self, broadcaster_id: str, id: Optional[str] = None, to
645651
)
646652

647653
async def post_automod_check(self, token: str, broadcaster_id: str, *msgs: List[Dict[str, str]]):
648-
print(msgs)
649654
return await self.request(
650655
Route(
651656
"POST",
@@ -656,6 +661,14 @@ async def post_automod_check(self, token: str, broadcaster_id: str, *msgs: List[
656661
)
657662
)
658663

664+
async def post_snooze_ad(self, token: str, broadcaster_id: str):
665+
q = [("broadcaster_id", broadcaster_id)]
666+
return await self.request(Route("POST", "channels/ads/schedule/snooze", query=q, token=token))
667+
668+
async def get_ad_schedule(self, token: str, broadcaster_id: str):
669+
q = [("broadcaster_id", broadcaster_id)]
670+
return await self.request(Route("GET", "channels/ads", query=q, token=token))
671+
659672
async def get_channel_ban_unban_events(self, token: str, broadcaster_id: str, user_ids: List[str] = None):
660673
q = [("broadcaster_id", broadcaster_id)]
661674
if user_ids:
@@ -668,6 +681,10 @@ async def get_channel_bans(self, token: str, broadcaster_id: str, user_ids: List
668681
q.extend(("user_id", id) for id in user_ids)
669682
return await self.request(Route("GET", "moderation/banned", query=q, token=token))
670683

684+
async def get_moderated_channels(self, token: str, user_id: str):
685+
q = [("user_id", user_id)]
686+
return await self.request(Route("GET", "moderation/channels", query=q, token=token))
687+
671688
async def get_channel_moderators(self, token: str, broadcaster_id: str, user_ids: List[str] = None):
672689
q = [("broadcaster_id", broadcaster_id)]
673690
if user_ids:
@@ -688,6 +705,13 @@ async def get_search_channels(self, query: str, token: str = None, live: bool =
688705
Route("GET", "search/channels", query=[("query", query), ("live_only", str(live))], token=token)
689706
)
690707

708+
async def get_user_emotes(self, user_id: str, broadcaster_id: Optional[str], token: str):
709+
q: List = [("user_id", user_id)]
710+
if broadcaster_id:
711+
q.append(("broadcaster_id", broadcaster_id))
712+
713+
return await self.request(Route("GET", "chat/emotes/user", query=q, token=token))
714+
691715
async def get_stream_key(self, token: str, broadcaster_id: str):
692716
return await self.request(
693717
Route("GET", "streams/key", query=[("broadcaster_id", broadcaster_id)], token=token), paginate=False

0 commit comments

Comments
 (0)