Skip to content

Commit 7bb21c2

Browse files
authored
Merge branch 'master' into sounds_setters
2 parents 16834d7 + c9cbe0a commit 7bb21c2

File tree

14 files changed

+463
-38
lines changed

14 files changed

+463
-38
lines changed

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2019 Myst(MysterialPy)
3+
Copyright (c) 2017 - Present PythonistaGuild
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

docs/changelog.rst

+17
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,26 @@
33
Master
44
======
55
- TwitchIO
6+
- Additions
7+
- Added :func:`~twitchio.Client.fetch_global_chat_badges`
8+
- Added User method :func:`~twitchio.PartialUser.fetch_chat_badges`
9+
- Added repr for :class:`~twitchio.SearchUser`
10+
- Added two new events
11+
- Added :func:`~twitchio.Client.event_notice`
12+
- Added :func:`~twitchio.Client.event_raw_notice`
13+
14+
- Added :class:`~twitchio.message.HypeChatData` for hype chat events
15+
- Added :attr:`~twitchio.message.Message.hype_chat_data` for hype chat events
16+
- Added :func:`~twitchio.Client.fetch_content_classification_labels` along with :class:`~twitchio.ContentClassificationLabel`
17+
- Added :attr:`~twitchio.ChannelInfo.content_classification_labels` and :attr:`~twitchio.ChannelInfo.is_branded_content` to :class:`~twitchio.ChannelInfo`
18+
- Added new parameters to :func:`~twitchio.PartialUser.modify_stream` for ``is_branded_content`` and ``content_classification_labels``
19+
620
- Bug fixes
721
- Fix :func:`~twitchio.Client.search_categories` due to :attr:`~twitchio.Game.igdb_id` being added to :class:`~twitchio.Game`
822
- Made Chatter :attr:`~twitchio.Chatter.id` property public
23+
- :func:`~twitchio.Client.event_token_expired` will now be called correctly when response is ``401 Invalid OAuth token``
24+
- Fix reconnect loop when Twitch sends a RECONNECT via IRC websocket
25+
- Fix :func:`~twitchio.CustomReward.edit` so it now can enable the reward
926

1027

1128
- ext.sounds

docs/reference.rst

+31
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,22 @@ ChannelTeams
9090
:members:
9191
:inherited-members:
9292

93+
ChatBadge
94+
----------
95+
.. attributetable:: ChatBadge
96+
97+
.. autoclass:: ChatBadge
98+
:members:
99+
:inherited-members:
100+
101+
ChatBadgeVersions
102+
------------------
103+
.. attributetable:: ChatBadgeVersions
104+
105+
.. autoclass:: ChatBadgeVersions
106+
:members:
107+
:inherited-members:
108+
93109
ChatSettings
94110
-------------
95111
.. attributetable:: ChatSettings
@@ -143,6 +159,14 @@ Clip
143159
:members:
144160
:inherited-members:
145161

162+
ContentClassificationLabel
163+
---------------------------
164+
.. attributetable:: ContentClassificationLabel
165+
166+
.. autoclass:: ContentClassificationLabel
167+
:members:
168+
:inherited-members:
169+
146170
CustomReward
147171
--------------
148172
.. attributetable:: CustomReward
@@ -207,6 +231,13 @@ Goal
207231
:members:
208232
:inherited-members:
209233

234+
HypeChatData
235+
236+
.. attributetable:: HypeChatData
237+
238+
.. autoclass:: HypeChatData
239+
:members:
240+
210241
HypeTrainContribution
211242
-----------------------
212243
.. attributetable:: HypeTrainContribution

twitchio/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
from .chatter import Chatter, PartialChatter
3737
from .enums import *
3838
from .errors import *
39-
from .message import Message
39+
from .message import Message, HypeChatData
4040
from .models import *
4141
from .rewards import *
4242
from .utils import *

twitchio/chatter.py

+3
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,9 @@ def __init__(self, websocket: "WSConnection", **kwargs):
135135
if self._badges:
136136
self._cached_badges = dict([badge.split("/") for badge in self._badges.split(",")])
137137

138+
def __repr__(self):
139+
return f"<Chatter name: {self._name}, channel: {self._channel}>"
140+
138141
def _bot_is_mod(self):
139142
cache = self._ws._cache[self._channel.name] # noqa
140143
for user in cache:

twitchio/client.py

+83
Original file line numberDiff line numberDiff line change
@@ -814,6 +814,38 @@ async def update_chatter_color(self, token: str, user_id: int, color: str):
814814
"""
815815
await self._http.put_user_chat_color(token=token, user_id=str(user_id), color=color)
816816

817+
async def fetch_global_chat_badges(self):
818+
"""|coro|
819+
820+
Fetches Twitch's list of chat badges, which users may use in any channel's chat room.
821+
822+
Returns
823+
--------
824+
List[:class:`twitchio.ChatBadge`]
825+
"""
826+
827+
data = await self._http.get_global_chat_badges()
828+
return [models.ChatBadge(x) for x in data]
829+
830+
async def fetch_content_classification_labels(self, locale: Optional[str] = None):
831+
"""|coro|
832+
833+
Fetches information about Twitch content classification labels.
834+
835+
Parameters
836+
-----------
837+
locale: Optional[:class:`str`]
838+
Locale for the Content Classification Labels.
839+
You may specify a maximum of 1 locale. Default: “en-US”
840+
841+
Returns
842+
--------
843+
List[:class:`twitchio.ContentClassificationLabel`]
844+
"""
845+
locale = "en-US" if locale is None else locale
846+
data = await self._http.get_content_classification_labels(locale)
847+
return [models.ContentClassificationLabel(x) for x in data]
848+
817849
async def get_webhook_subscriptions(self):
818850
"""|coro|
819851
@@ -1043,3 +1075,54 @@ async def event_channel_join_failure(self, channel: str):
10431075
The channel name that was attempted to be joined.
10441076
"""
10451077
logger.error(f'The channel "{channel}" was unable to be joined. Check the channel is valid.')
1078+
1079+
async def event_raw_notice(self, data: str):
1080+
"""|coro|
1081+
1082+
1083+
Event called with the raw NOTICE data received by Twitch.
1084+
1085+
Parameters
1086+
------------
1087+
data: str
1088+
The raw NOTICE data received from Twitch.
1089+
1090+
Example
1091+
---------
1092+
.. code:: py
1093+
1094+
@bot.event()
1095+
async def event_raw_notice(data):
1096+
print(data)
1097+
"""
1098+
pass
1099+
1100+
async def event_notice(self, message: str, msg_id: Optional[str], channel: Optional[Channel]):
1101+
"""|coro|
1102+
1103+
1104+
Event called with the NOTICE data received by Twitch.
1105+
1106+
.. tip::
1107+
1108+
For more information on NOTICE msg_ids visit:
1109+
https://dev.twitch.tv/docs/irc/msg-id/
1110+
1111+
Parameters
1112+
------------
1113+
message: :class:`str`
1114+
The message of the NOTICE.
1115+
msg_id: Optional[:class:`str`]
1116+
The msg_id that indicates what the NOTICE type.
1117+
channel: Optional[:class:`~twitchio.Channel`]
1118+
The channel the NOTICE message originated from.
1119+
1120+
Example
1121+
---------
1122+
.. code:: py
1123+
1124+
@bot.event()
1125+
async def event_notice(message, msg_id, channel):
1126+
print(message)
1127+
"""
1128+
pass

twitchio/ext/commands/core.py

+49
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,25 @@ def _boolconverter(param: str):
5252

5353

5454
class Command:
55+
"""A class for implementing bot commands.
56+
57+
Parameters
58+
------------
59+
name: :class:`str`
60+
The name of the command.
61+
func: :class:`Callable`
62+
The coroutine that executes when the command is invoked.
63+
64+
Attributes
65+
------------
66+
name: :class:`str`
67+
The name of the command.
68+
cog: :class:`~twitchio.ext.commands.Cog`
69+
The cog this command belongs to.
70+
aliases: Optional[Union[:class:`list`, :class:`tuple`]]
71+
Aliases that can be used to also invoke the command.
72+
"""
73+
5574
def __init__(self, name: str, func: Callable, **attrs) -> None:
5675
if not inspect.iscoroutinefunction(func):
5776
raise TypeError("Command callback must be a coroutine.")
@@ -335,6 +354,36 @@ def decorator(func: Callable):
335354

336355

337356
class Context(Messageable):
357+
"""
358+
A class that represents the context in which a command is being invoked under.
359+
360+
This class contains the meta data to help you understand more about the invocation context.
361+
This class is not created manually and is instead passed around to commands as the first parameter.
362+
363+
Attributes
364+
-----------
365+
message: :class:`~twitchio.Message`
366+
The message that triggered the command being executed.
367+
channel: :class:`~twitchio.Channel`
368+
The channel the command was invoked in.
369+
author: Union[:class:`~twitchio.PartialChatter`, :class:`~twitchio.Chatter`]
370+
The Chatter object of the user in chat that invoked the command.
371+
prefix: Optional[:class:`str`]
372+
The prefix that was used to invoke the command.
373+
command: Optional[:class:`~twitchio.ext.commands.Command`]
374+
The command that was invoked
375+
cog: Optional[:class:`~twitchio.ext.commands.Cog`]
376+
The cog that contains the command that was invoked.
377+
args: Optional[List[:class:`Any`]]
378+
List of arguments that were passed to the command.
379+
kwargs: Optional[Dict[:class:`str`, :class:`Any`]]
380+
List of kwargs that were passed to the command.
381+
view: Optional[:class:`~twitchio.ext.commmands.StringParser`]
382+
StringParser object that breaks down the command string received.
383+
bot: :class:`~twitchio.ext.commands.Bot`
384+
The bot that contains the command that was invoked.
385+
"""
386+
338387
__messageable_channel__ = True
339388

340389
def __init__(self, message: Message, bot: Bot, **attrs) -> None:

twitchio/ext/pubsub/pool.py

+23-10
Original file line numberDiff line numberDiff line change
@@ -104,20 +104,32 @@ async def _process_auth_fail(self, nonce: str, node: PubSubWebsocket) -> None:
104104
logger.error("Error occurred while calling auth_fail_hook.", exc_info=e)
105105

106106
async def auth_fail_hook(self, topics: List[Topic]):
107-
"""
107+
"""|coro|
108108
This is a hook that can be overridden in a subclass.
109+
From this hook, you can refresh expired tokens (or prompt a user for new ones), and resubscribe to the events.
110+
111+
.. note::
112+
113+
The topics will not be automatically resubscribed to. You must do it yourself by calling :meth:`~PubSubPool.subscribe_topics` with the topics after obtaining new tokens.
114+
115+
An example of what this method should do:
116+
117+
.. code:: python
118+
119+
class MyPubSubPool(pubsub.PubSubPool):
120+
async def auth_fail_hook(self, topics: List[pubsub.Topic]):
121+
token = topics[0].token
122+
new_token = await some_imaginary_function_that_refreshes_tokens(token)
109123
124+
for topic in topics:
125+
topic.token = new_token
126+
127+
await self.subscribe_topics(topics)
110128
111129
Parameters
112130
----------
113-
node
114131
topics: List[:class:`Topic`]
115-
The topcs that this node has.
116-
117-
Returns
118-
-------
119-
List[:class:`Topic`]
120-
The list of topics this node should have. Any additions, modifications, or removals will be respected.
132+
The topics that have been deauthorized. Typically these will all contain the same token.
121133
"""
122134

123135
async def _process_reconnect_hook(self, node: PubSubWebsocket) -> None:
@@ -145,9 +157,10 @@ async def reconnect_hook(self, node: PubSubWebsocket, topics: List[Topic]) -> Li
145157
146158
Parameters
147159
----------
148-
node
160+
node: :class:`PubSubWebsocket`
161+
The node that is reconnecting.
149162
topics: List[:class:`Topic`]
150-
The topcs that this node has.
163+
The topics that this node has.
151164
152165
Returns
153166
-------

twitchio/http.py

+31-6
Original file line numberDiff line numberDiff line change
@@ -207,14 +207,15 @@ async def _request(self, route, path, headers, utilize_bucket=True):
207207
return await resp.json(), False
208208
return await resp.text(encoding="utf-8"), True
209209
if resp.status == 401:
210-
if "WWW-Authenticate" in resp.headers:
210+
message_json = await resp.json()
211+
if "Invalid OAuth token" in message_json.get("message", ""):
211212
try:
212213
await self._generate_login()
213214
except:
214215
raise errors.Unauthorized(
215216
"Your oauth token is invalid, and a new one could not be generated"
216217
)
217-
print(resp.reason, await resp.json(), resp)
218+
print(resp.reason, message_json, resp)
218219
raise errors.Unauthorized("You're not authorized to use this route.")
219220
if resp.status == 429:
220221
reason = "Ratelimit Reached"
@@ -420,7 +421,7 @@ async def update_reward(
420421
"prompt": prompt,
421422
"cost": cost,
422423
"background_color": background_color,
423-
"enabled": enabled,
424+
"is_enabled": enabled,
424425
"is_user_input_required": input_required,
425426
"is_max_per_stream_enabled": max_per_stream_enabled,
426427
"max_per_stream": max_per_stream,
@@ -716,15 +717,30 @@ async def get_channels_new(self, broadcaster_ids: List[int], token: Optional[str
716717
return await self.request(Route("GET", "channels", query=q, token=token))
717718

718719
async def patch_channel(
719-
self, token: str, broadcaster_id: str, game_id: str = None, language: str = None, title: str = None
720+
self,
721+
token: str,
722+
broadcaster_id: str,
723+
game_id: str = None,
724+
language: str = None,
725+
title: str = None,
726+
content_classification_labels: List[Dict[str, Union[str, bool]]] = None,
727+
is_branded_content: bool = None,
720728
):
721-
assert any((game_id, language, title))
729+
assert any((game_id, language, title, content_classification_labels, is_branded_content))
722730
body = {
723731
k: v
724-
for k, v in {"game_id": game_id, "broadcaster_language": language, "title": title}.items()
732+
for k, v in {
733+
"game_id": game_id,
734+
"broadcaster_language": language,
735+
"title": title,
736+
"is_branded_content": is_branded_content,
737+
}.items()
725738
if v is not None
726739
}
727740

741+
if content_classification_labels is not None:
742+
body["content_classification_labels"] = content_classification_labels
743+
728744
return await self.request(
729745
Route("PATCH", "channels", query=[("broadcaster_id", broadcaster_id)], body=body, token=token)
730746
)
@@ -1129,3 +1145,12 @@ async def post_shoutout(self, token: str, broadcaster_id: str, moderator_id: str
11291145
("to_broadcaster_id", to_broadcaster_id),
11301146
]
11311147
return await self.request(Route("POST", "chat/shoutouts", query=q, token=token))
1148+
1149+
async def get_global_chat_badges(self):
1150+
return await self.request(Route("GET", "chat/badges/global", ""))
1151+
1152+
async def get_channel_chat_badges(self, broadcaster_id: str):
1153+
return await self.request(Route("GET", "chat/badges", "", query=[("broadcaster_id", broadcaster_id)]))
1154+
1155+
async def get_content_classification_labels(self, locale: str):
1156+
return await self.request(Route("GET", "content_classification_labels", "", query=[("locale", locale)]))

0 commit comments

Comments
 (0)