From bdb21b036ed89eabc2468e132edc8737ee8ea741 Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Mon, 15 Jan 2024 15:48:38 -0800 Subject: [PATCH 01/36] refactor eventsubws subscription error handling to not error on reconnect --- twitchio/ext/eventsub/websocket.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/twitchio/ext/eventsub/websocket.py b/twitchio/ext/eventsub/websocket.py index 2f982357..f9568958 100644 --- a/twitchio/ext/eventsub/websocket.py +++ b/twitchio/ext/eventsub/websocket.py @@ -4,7 +4,7 @@ import logging import aiohttp -from typing import Optional, TYPE_CHECKING, Tuple, Type, Dict, Callable, Generic, TypeVar, Awaitable, Union, cast, List +from typing import Optional, TYPE_CHECKING, Tuple, Type, Dict, Callable, Generic, TypeVar, Awaitable, Union, cast, List, Literal from . import models, http from .models import _loads from twitchio import PartialUser, Unauthorized, HTTPException @@ -32,7 +32,7 @@ def __init__(self, event_type: Tuple[str, int, Type[models.EventData]], conditio self.token = token self.subscription_id: Optional[str] = None self.cost: Optional[int] = None - self.created: asyncio.Future[Tuple[bool, int]] | None = asyncio.Future() + self.created: asyncio.Future[Tuple[Literal[False], int] | Tuple[Literal[True], None]] | None = asyncio.Future() _T = TypeVar("_T") @@ -117,18 +117,20 @@ async def _subscribe(self, obj: _Subscription) -> dict | None: try: resp = await self._http.create_websocket_subscription(obj.event, obj.condition, self._session_id, obj.token) except HTTPException as e: - assert obj.created - obj.created.set_result((False, e.status)) # type: ignore + if obj.created: + obj.created.set_result((False, e.status)) + + else: + logger.error("An error (%s %s) occurred while attempting to resubscribe to an event on reconnect: %s", e.status, e.reason, e.message) + return None - else: - assert obj.created - obj.created.set_result((True, None)) # type: ignore + if obj.created: + obj.created.set_result((True, None)) data = resp["data"][0] - cost = data["cost"] self.remaining_slots = resp["max_total_cost"] - resp["total_cost"] - obj.cost = cost + obj.cost = data["cost"] return data From fc7cdfefc4c0a51e37f6aba0cd114f520868eb7e Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Mon, 4 Mar 2024 15:37:19 -0800 Subject: [PATCH 02/36] potential fix for bug with headers not getting set after token updates --- twitchio/http.py | 42 ++++++++++++++++++++++++------------------ 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/twitchio/http.py b/twitchio/http.py index 2276af5c..fe90ca21 100644 --- a/twitchio/http.py +++ b/twitchio/http.py @@ -121,22 +121,8 @@ async def request(self, route: Route, *, paginate=True, limit=100, full_body=Fal raise errors.NoClientID("A Client ID is required to use the Twitch API") headers = route.headers or {} - if force_app_token and "Authorization" not in headers: - if not self.client_secret: - raise errors.NoToken( - "An app access token is required for this route, please provide a client id and client secret" - ) - if self.app_token is None: - await self._generate_login() - headers["Authorization"] = f"Bearer {self.app_token}" - elif not self.token and not self.client_secret and "Authorization" not in headers: - raise errors.NoToken( - "Authorization is required to use the Twitch API. Pass token and/or client_secret to the Client constructor" - ) - if "Authorization" not in headers: - if not self.token: - await self._generate_login() - headers["Authorization"] = f"Bearer {self.token}" + await self._apply_auth(headers, force_app_token, False) + headers["Client-ID"] = self.client_id if not self.session: @@ -165,7 +151,7 @@ def get_limit(): q = [("after", cursor), *q] q = [("first", get_limit()), *q] path = path.with_query(q) - body, is_text = await self._request(route, path, headers) + body, is_text = await self._request(route, path, headers, force_app_token=force_app_token) if is_text: return body if full_body: @@ -182,7 +168,26 @@ def get_limit(): is_finished = reached_limit() if limit is not None else True if paginate else True return data - async def _request(self, route, path, headers, utilize_bucket=True): + async def _apply_auth(self, headers: dict, force_app_token: bool, force_apply: bool) -> None: + if force_app_token and "Authorization" not in headers: + if not self.client_secret: + raise errors.NoToken( + "An app access token is required for this route, please provide a client id and client secret" + ) + if self.app_token is None: + await self._generate_login() + headers["Authorization"] = f"Bearer {self.app_token}" + elif not self.token and not self.client_secret and "Authorization" not in headers: + raise errors.NoToken( + "Authorization is required to use the Twitch API. Pass token and/or client_secret to the Client constructor" + ) + if "Authorization" not in headers or force_apply: + if not self.token: + await self._generate_login() + + headers["Authorization"] = f"Bearer {self.token}" + + async def _request(self, route, path, headers, utilize_bucket=True, force_app_token: bool = False): reason = None for attempt in range(5): @@ -224,6 +229,7 @@ async def _request(self, route, path, headers, utilize_bucket=True): if "Invalid OAuth token" in message_json.get("message", ""): try: await self._generate_login() + await self._apply_auth(headers, force_app_token, True) continue except: raise errors.Unauthorized( From 2149f5e556a903ed81aaa19b51623ae20dd22e2e Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Thu, 14 Mar 2024 20:14:10 -0700 Subject: [PATCH 03/36] Revert "refactor eventsubws subscription error handling to not error on reconnect" This reverts commit bdb21b036ed89eabc2468e132edc8737ee8ea741. --- twitchio/ext/eventsub/websocket.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/twitchio/ext/eventsub/websocket.py b/twitchio/ext/eventsub/websocket.py index f9568958..2f982357 100644 --- a/twitchio/ext/eventsub/websocket.py +++ b/twitchio/ext/eventsub/websocket.py @@ -4,7 +4,7 @@ import logging import aiohttp -from typing import Optional, TYPE_CHECKING, Tuple, Type, Dict, Callable, Generic, TypeVar, Awaitable, Union, cast, List, Literal +from typing import Optional, TYPE_CHECKING, Tuple, Type, Dict, Callable, Generic, TypeVar, Awaitable, Union, cast, List from . import models, http from .models import _loads from twitchio import PartialUser, Unauthorized, HTTPException @@ -32,7 +32,7 @@ def __init__(self, event_type: Tuple[str, int, Type[models.EventData]], conditio self.token = token self.subscription_id: Optional[str] = None self.cost: Optional[int] = None - self.created: asyncio.Future[Tuple[Literal[False], int] | Tuple[Literal[True], None]] | None = asyncio.Future() + self.created: asyncio.Future[Tuple[bool, int]] | None = asyncio.Future() _T = TypeVar("_T") @@ -117,20 +117,18 @@ async def _subscribe(self, obj: _Subscription) -> dict | None: try: resp = await self._http.create_websocket_subscription(obj.event, obj.condition, self._session_id, obj.token) except HTTPException as e: - if obj.created: - obj.created.set_result((False, e.status)) - - else: - logger.error("An error (%s %s) occurred while attempting to resubscribe to an event on reconnect: %s", e.status, e.reason, e.message) - + assert obj.created + obj.created.set_result((False, e.status)) # type: ignore return None - if obj.created: - obj.created.set_result((True, None)) + else: + assert obj.created + obj.created.set_result((True, None)) # type: ignore data = resp["data"][0] + cost = data["cost"] self.remaining_slots = resp["max_total_cost"] - resp["total_cost"] - obj.cost = data["cost"] + obj.cost = cost return data From 164d884378c2b5f110a82d966a1b486e78933f02 Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Thu, 14 Mar 2024 20:17:31 -0700 Subject: [PATCH 04/36] formatting --- twitchio/http.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twitchio/http.py b/twitchio/http.py index fe90ca21..5005ef20 100644 --- a/twitchio/http.py +++ b/twitchio/http.py @@ -184,7 +184,7 @@ async def _apply_auth(self, headers: dict, force_app_token: bool, force_apply: b if "Authorization" not in headers or force_apply: if not self.token: await self._generate_login() - + headers["Authorization"] = f"Bearer {self.token}" async def _request(self, route, path, headers, utilize_bucket=True, force_app_token: bool = False): From edc590294c76eb30a0ea181ebdc4c6cc233f482b Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Thu, 14 Mar 2024 20:20:45 -0700 Subject: [PATCH 05/36] changelog entry for both prs --- docs/changelog.rst | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index d7cdcd2d..c57f4515 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,16 @@ :orphan: +Master +======= +- TwitchIO + - Bug fixes + - Fixed ``event_token_expired`` not applying to the current request. + +- ext.eventsub + - Bug fixes + - Fixed a crash where a Future could be None, causing unintentional errors. + + 2.8.2 ====== - ext.commands From de46ee6dda27395106f23b563749e5f2ea3fba5e Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Thu, 14 Mar 2024 21:23:31 -0700 Subject: [PATCH 06/36] add more changelogs --- docs/changelog.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index c57f4515..5e835c92 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,8 +1,15 @@ :orphan: -Master +2.9.0 ======= - TwitchIO + - Additions + - Added :class:`~twitchio.AdSchedule` + - Added the new ad-related methods for :class:`~twitchio.PartialUser`: + - :func:`~twitchio.PartialUser.fetch_ad_schedule` + - :func:`~twitchio.PartialUser.snooze_ad` + - Added :func:`~twitchio.PartialUser.fetch_moderated_channels` to :class:`~twitchio.PartialUser` + - Bug fixes - Fixed ``event_token_expired`` not applying to the current request. From cb859e3213da01319b3686fa4d143f8830169202 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 15 Mar 2024 14:43:43 -0700 Subject: [PATCH 07/36] refactor eventsubws subscription error handling to not error on reconnect (#439) * refactor eventsubws subscription error handling to not error on reconnect * Why do we still support 3.7 * formatting --- twitchio/ext/eventsub/websocket.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/twitchio/ext/eventsub/websocket.py b/twitchio/ext/eventsub/websocket.py index 2f982357..5eedc621 100644 --- a/twitchio/ext/eventsub/websocket.py +++ b/twitchio/ext/eventsub/websocket.py @@ -10,6 +10,7 @@ from twitchio import PartialUser, Unauthorized, HTTPException if TYPE_CHECKING: + from typing_extensions import Literal from twitchio import Client logger = logging.getLogger("twitchio.ext.eventsub.ws") @@ -32,7 +33,7 @@ def __init__(self, event_type: Tuple[str, int, Type[models.EventData]], conditio self.token = token self.subscription_id: Optional[str] = None self.cost: Optional[int] = None - self.created: asyncio.Future[Tuple[bool, int]] | None = asyncio.Future() + self.created: asyncio.Future[Tuple[Literal[False], int] | Tuple[Literal[True], None]] | None = asyncio.Future() _T = TypeVar("_T") @@ -117,18 +118,25 @@ async def _subscribe(self, obj: _Subscription) -> dict | None: try: resp = await self._http.create_websocket_subscription(obj.event, obj.condition, self._session_id, obj.token) except HTTPException as e: - assert obj.created - obj.created.set_result((False, e.status)) # type: ignore + if obj.created: + obj.created.set_result((False, e.status)) + + else: + logger.error( + "An error (%s %s) occurred while attempting to resubscribe to an event on reconnect: %s", + e.status, + e.reason, + e.message, + ) + return None - else: - assert obj.created - obj.created.set_result((True, None)) # type: ignore + if obj.created: + obj.created.set_result((True, None)) data = resp["data"][0] - cost = data["cost"] self.remaining_slots = resp["max_total_cost"] - resp["total_cost"] - obj.cost = cost + obj.cost = data["cost"] return data From 417584d311437ee5e44908cce71a6f9c635b6e58 Mon Sep 17 00:00:00 2001 From: Tom Date: Fri, 15 Mar 2024 16:12:08 -0700 Subject: [PATCH 08/36] Add new API routes (#441) * Add new API routes * add docs --- docs/reference.rst | 7 ++++++ twitchio/http.py | 13 +++++++++- twitchio/models.py | 39 ++++++++++++++++++++++++++++++ twitchio/user.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 1 deletion(-) diff --git a/docs/reference.rst b/docs/reference.rst index 9c70cbb3..a9784ac0 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -11,6 +11,13 @@ ActiveExtension :members: :inherited-members: +AdSchedule +------------ +.. attributetable:: AdSchedule + +.. autoclass:: AdSchedule + :members: + AutomodCheckMessage --------------------- .. attributetable:: AutomodCheckMessage diff --git a/twitchio/http.py b/twitchio/http.py index 5005ef20..c38a2741 100644 --- a/twitchio/http.py +++ b/twitchio/http.py @@ -651,7 +651,6 @@ async def get_hype_train(self, broadcaster_id: str, id: Optional[str] = None, to ) async def post_automod_check(self, token: str, broadcaster_id: str, *msgs: List[Dict[str, str]]): - print(msgs) return await self.request( Route( "POST", @@ -662,6 +661,14 @@ async def post_automod_check(self, token: str, broadcaster_id: str, *msgs: List[ ) ) + async def post_snooze_ad(self, token: str, broadcaster_id: str): + q = [("broadcaster_id", broadcaster_id)] + return await self.request(Route("POST", "channels/ads/schedule/snooze", query=q, token=token)) + + async def get_ad_schedule(self, token: str, broadcaster_id: str): + q = [("broadcaster_id", broadcaster_id)] + return await self.request(Route("GET", "channels/ads", query=q, token=token)) + async def get_channel_ban_unban_events(self, token: str, broadcaster_id: str, user_ids: List[str] = None): q = [("broadcaster_id", broadcaster_id)] if user_ids: @@ -674,6 +681,10 @@ async def get_channel_bans(self, token: str, broadcaster_id: str, user_ids: List q.extend(("user_id", id) for id in user_ids) return await self.request(Route("GET", "moderation/banned", query=q, token=token)) + async def get_moderated_channels(self, token: str, user_id: str): + q = [("user_id", user_id)] + return await self.request(Route("GET", "moderation/channels", query=q, token=token)) + async def get_channel_moderators(self, token: str, broadcaster_id: str, user_ids: List[str] = None): q = [("broadcaster_id", broadcaster_id)] if user_ids: diff --git a/twitchio/models.py b/twitchio/models.py index d2f027e1..0f722b54 100644 --- a/twitchio/models.py +++ b/twitchio/models.py @@ -33,6 +33,7 @@ if TYPE_CHECKING: from .http import TwitchHTTP __all__ = ( + "AdSchedule", "BitsLeaderboard", "Clip", "CheerEmote", @@ -87,6 +88,44 @@ ) +class AdSchedule: + """ + Represents a channel's ad schedule. + + Attributes + ----------- + next_ad_at: Optional[:class:`datetime.datetime`] + When the next ad will roll. Will be ``None`` if the streamer does not schedule ads, or is not live. + last_ad_at: Optional[:class:`datetime.datetime`] + When the last ad rolled. Will be ``None`` if the streamer has not rolled an ad. + Will always be ``None`` when this comes from snoozing an ad. + duration: :class:`int` + How long the upcoming ad will be, in seconds. + preroll_freeze_time: Optional[:class:`int`] + The amount of pre-roll free time remaining for the channel in seconds. Will be 0 if the streamer is not pre-roll free. + Will be ``None`` when this comes from snoozing an ad. + snooze_count: :class:`int` + How many snoozes the streamer has left. + snooze_refresh_at: :class:`datetime.datetime` + When the streamer will gain another snooze. + """ + + __slots__ = "next_ad_at", "last_ad_at", "duration", "preroll_freeze_time", "snooze_count", "snooze_refresh_at" + + def __init__(self, data: dict) -> None: + self.duration: int = data["duration"] + self.preroll_freeze_time: int = data["preroll_freeze_time"] + self.snooze_count: int = data["snooze_count"] + + self.snooze_refresh_at: datetime.datetime = parse_timestamp(data["snooze_refresh_at"]) + self.next_ad_at: Optional[datetime.datetime] = ( + parse_timestamp(data["next_ad_at"]) if data["next_ad_at"] else None + ) + self.last_ad_at: Optional[datetime.datetime] = ( + parse_timestamp(data["last_ad_at"]) if data["last_ad_at"] else None + ) + + class BitsLeaderboard: """ Represents a Bits leaderboard from the twitch API. diff --git a/twitchio/user.py b/twitchio/user.py index 86b39018..7a06c2a2 100644 --- a/twitchio/user.py +++ b/twitchio/user.py @@ -37,6 +37,7 @@ from .http import TwitchHTTP from .channel import Channel from .models import ( + AdSchedule, BitsLeaderboard, Clip, ExtensionBuilder, @@ -334,6 +335,48 @@ async def create_clip(self, token: str, has_delay=False) -> dict: data = await self._http.post_create_clip(token, self.id, has_delay) return data[0] + async def fetch_ad_schedule(self, token: str) -> AdSchedule: + """|coro| + + Fetches the streamers's ad schedule. + + Parameters + ----------- + token: :class:`str` + The user's oauth token with the ``channel:read:ads`` scope. + + Returns + -------- + :class:`twitchio.AdSchedule` + """ + from .models import AdSchedule + + data = await self._http.get_ad_schedule(token, str(self.id)) + return AdSchedule(data[0]) + + async def snooze_ad(self, token: str) -> List[AdSchedule]: + """|coro| + + Snoozes an ad on the streamer's channel. + + .. note:: + The resulting :class:`~twitchio.AdSchedule` only has data for the :attr:`~twitchio.AdSchedule.snooze_count`, + :attr:`~twitchio.AdSchedule.snooze_refresh_at`, and :attr:`~twitchio.AdSchedule.next_ad_at` attributes. + + Parameters + ----------- + token: :class:`str` + The user's oauth token with the ``channel:manage:ads`` scope. + + Returns + -------- + :class:`twitchio.AdSchedule` + """ + from .models import AdSchedule + + data = await self._http.post_snooze_ad(token, str(self.id)) + return AdSchedule(data[0]) + async def fetch_clips( self, started_at: Optional[datetime.datetime] = None, @@ -403,6 +446,23 @@ async def fetch_bans(self, token: str, userids: List[Union[str, int]] = None) -> data = await self._http.get_channel_bans(token, str(self.id), user_ids=userids) return [UserBan(self._http, d) for d in data] + async def fetch_moderated_channels(self, token: str) -> List[PartialUser]: + """|coro| + + Fetches channels that this user moderates. + + Parameters + ----------- + token: :class:`str` + An oauth token for this user with the ``user:read:moderated_channels`` scope. + + Returns + -------- + List[:class:`twitchio.PartialUser`] + """ + data = await self._http.get_moderated_channels(token, str(self.id)) + return [PartialUser(self._http, d["user_id"], d["user_name"]) for d in data] + async def fetch_moderators(self, token: str, userids: List[int] = None): """|coro| From f56b13f603299bb2e50480901796c4f321e408fa Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Sat, 16 Mar 2024 17:43:48 -0700 Subject: [PATCH 09/36] Add user emote endpoint --- docs/changelog.rst | 3 +- docs/reference.rst | 7 ++++ twitchio/http.py | 7 ++++ twitchio/models.py | 101 +++++++++++++++++++++++++++++++++++++++++++++ twitchio/user.py | 31 ++++++++++++++ 5 files changed, 148 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5e835c92..bba9d484 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,10 +4,11 @@ ======= - TwitchIO - Additions - - Added :class:`~twitchio.AdSchedule` + - Added :class:`~twitchio.AdSchedule` and :class:`~twitchio.Emote` - Added the new ad-related methods for :class:`~twitchio.PartialUser`: - :func:`~twitchio.PartialUser.fetch_ad_schedule` - :func:`~twitchio.PartialUser.snooze_ad` + - Added new method :func:`~twitchio.PartialUser.fetch_user_emotes` to :class:`~twitchio.PartialUser` - Added :func:`~twitchio.PartialUser.fetch_moderated_channels` to :class:`~twitchio.PartialUser` - Bug fixes diff --git a/docs/reference.rst b/docs/reference.rst index a9784ac0..c4d7b28a 100644 --- a/docs/reference.rst +++ b/docs/reference.rst @@ -220,6 +220,13 @@ CustomRewardRedemption :members: :inherited-members: +Emote +------ +.. attributetable:: Emote + +.. autoclass:: Emote + :members: + Extension ----------- .. attributetable:: Extension diff --git a/twitchio/http.py b/twitchio/http.py index c38a2741..ec713081 100644 --- a/twitchio/http.py +++ b/twitchio/http.py @@ -705,6 +705,13 @@ async def get_search_channels(self, query: str, token: str = None, live: bool = Route("GET", "search/channels", query=[("query", query), ("live_only", str(live))], token=token) ) + async def get_user_emotes(self, user_id: str, broadcaster_id: Optional[str], token: str): + q: List = [("user_id", user_id)] + if broadcaster_id: + q.append(("broadcaster_id", broadcaster_id)) + + return await self.request(Route("GET", "chat/emotes/user", query=q, token=token)) + async def get_stream_key(self, token: str, broadcaster_id: str): return await self.request( Route("GET", "streams/key", query=[("broadcaster_id", broadcaster_id)], token=token), paginate=False diff --git a/twitchio/models.py b/twitchio/models.py index 0f722b54..483f20ea 100644 --- a/twitchio/models.py +++ b/twitchio/models.py @@ -31,13 +31,16 @@ from .user import BitLeaderboardUser, PartialUser, User if TYPE_CHECKING: + from typing_extensions import Literal from .http import TwitchHTTP + __all__ = ( "AdSchedule", "BitsLeaderboard", "Clip", "CheerEmote", "CheerEmoteTier", + "Emote", "GlobalEmote", "ChannelEmote", "HypeTrainContribution", @@ -651,6 +654,104 @@ def __repr__(self): ) +class Emote: + """ + Represents an Emote. + + .. note:: + + It seems twitch is sometimes returning duplicate information from the emotes endpoint. + To deduplicate your emotes, you can call ``set()`` on the list of emotes (or any other hashmap), which will remove the duplicates. + + .. code-block:: python + + my_list_of_emotes = await user.get_user_emotes(...) + deduplicated_emotes = set(my_list_of_emotes) + + Attributes + ----------- + id: :class:`str` + The unique ID of the emote. + set_id: Optional[:class:`str`] + The ID of the set this emote belongs to. + Will be ``None`` if the emote doesn't belong to a set. + owner_id: Optional[:class:`str`] + The ID of the channel this emote belongs to. + name: :class:`str` + The name of this emote, as the user sees it. + type: :class:`str` + The reason this emote is available to the user. + Some available values (twitch hasn't documented this properly, there might be more): + + - follower + - subscription + - bitstier + - hypetrain + - globals (global emotes) + + scales: list[:class:`str`] + The available scaling for this emote. These are typically floats (ex. "1.0", "2.0"). + format_static: :class:`bool` + Whether this emote is available as a static (PNG) file. + format_animated: :class:`bool` + Whether this emote is available as an animated (GIF) file. + theme_light: :class:`bool` + Whether this emote is available in light theme background mode. + theme_dark: :class:`bool` + Whether this emote is available in dark theme background mode. + """ + + __slots__ = "id", "set_id", "owner_id", "name", "type", "scales", "format_static", "format_animated", "theme_light", "theme_dark" + + def __init__(self, data: dict) -> None: + self.id: str = data["id"] + self.set_id: Optional[str] = data["emote_set_id"] and None + self.owner_id: Optional[str] = data["owner_id"] and None + self.name: str = data["name"] + self.type: str = data["emote_type"] + self.scales: List[str] = data["scale"] + self.theme_dark: bool = "dark" in data["theme_mode"] + self.theme_light: bool = "light" in data["theme_mode"] + self.format_static: bool = "static" in data["format"] + self.format_animated: bool = "animated" in data["format"] + + def url_for(self, format: Literal["static", "animated"], theme: Literal["dark", "light"], scale: str) -> str: + """ + Returns a cdn url that can be used to download or serve the emote on a website. + This function validates that the arguments passed are possible values to serve the emote. + + Parameters + ----------- + format: Literal["static", "animated"] + The format of the emote. You can check what formats are available using :attr:`~.format_static` and :attr:`~.format_animated`. + theme: Literal["dark", "light"] + The theme of the emote. You can check what themes are available using :attr:`~.format_dark` and :attr:`~.format_light`. + scale: :class:`str` + The scale of the emote. This should be formatted in this format: ``"1.0"``. + The scales available for this emote can be checked via :attr:`~.scales`. + + Returns + -------- + :class:`str` + """ + if scale not in self.scales: + raise ValueError(f"scale for this emote must be one of {', '.join(self.scales)}, not {scale}") + + if (theme == "dark" and not self.theme_dark) or (theme == "light" and not self.theme_light): + raise ValueError(f"theme {theme} is not an available value for this emote") + + if (format == "static" and not self.format_static) or (format == "animated" and not self.format_animated): + raise ValueError(f"format {format} is not an available value for this emote") + + return f"https://static-cdn.jtvnw.net/emoticons/v2/{self.id}/{format}/{theme}/{scale}" + + def __repr__(self) -> str: + return f"" + + def __hash__(self) -> int: # this exists so we can do set(list of emotes) to get rid of duplicates + return hash(self.id) + + class Marker: """ Represents a stream Marker diff --git a/twitchio/user.py b/twitchio/user.py index 7a06c2a2..14595398 100644 --- a/twitchio/user.py +++ b/twitchio/user.py @@ -40,6 +40,7 @@ AdSchedule, BitsLeaderboard, Clip, + Emote, ExtensionBuilder, Tag, FollowEvent, @@ -638,6 +639,32 @@ async def fetch_channel_emotes(self): data = await self._http.get_channel_emotes(str(self.id)) return [ChannelEmote(self._http, x) for x in data] + + async def fetch_user_emotes(self, token: str, broadcaster: Optional[PartialUser] = None) -> List[Emote]: + """|coro| + + Fetches emotes the user has access to. Optionally, you can filter by a broadcaster. + + .. note:: + + As of writing, this endpoint seems extrememly unoptimized by twitch, and may (read: will) take a lot of API requests to load. + See https://github.com/twitchdev/issues/issues/921 . + + Parameters + ----------- + token: :class:`str` + An OAuth token belonging to this user with the ``user:read:emotes`` scope. + broadcaster: Optional[:class:`~twitchio.PartialUser`] + A channel to filter the results with. + Filtering will return all emotes available to the user on that channel, including global emotes. + + Returns + -------- + List[:class:`~twitchio.Emote`] + """ + from .models import Emote + data = await self._http.get_user_emotes(str(self.id), broadcaster and str(broadcaster.id), token) + return [Emote(d) for d in data] async def follow(self, userid: int, token: str, *, notifications=False): """|coro| @@ -666,6 +693,10 @@ async def unfollow(self, userid: int, token: str): Unfollows the user + .. warning:: + + This method is obsolete as Twitch removed the endpoint. + Parameters ----------- userid: :class:`int` From 1c2333ae5dd91d56678fb8ec322a363042a80d4f Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Sat, 16 Mar 2024 17:47:52 -0700 Subject: [PATCH 10/36] work around bad frame disconnect --- docs/changelog.rst | 1 + twitchio/ext/eventsub/websocket.py | 7 ++++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index bba9d484..c437f787 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -17,6 +17,7 @@ - ext.eventsub - Bug fixes - Fixed a crash where a Future could be None, causing unintentional errors. + - Special-cased a restart when a specific known bad frame is received. 2.8.2 diff --git a/twitchio/ext/eventsub/websocket.py b/twitchio/ext/eventsub/websocket.py index 5eedc621..4c3d889f 100644 --- a/twitchio/ext/eventsub/websocket.py +++ b/twitchio/ext/eventsub/websocket.py @@ -209,9 +209,14 @@ async def pump(self) -> None: await self.connect() return - except TypeError as e: + except TypeError as e: logger.warning(f"Received bad frame: {e.args[0]}") + if e.args[0] is None: # websocket was closed, reconnect + logger.info("Known bad frame, restarting connection") + await self.connect() + return + except Exception as e: logger.error("Exception in the pump function!", exc_info=e) raise From 9abe3ce8bc40f186df9403b2ae3a6fbcc857aef0 Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Sun, 17 Mar 2024 07:48:19 -0700 Subject: [PATCH 11/36] run black --- twitchio/ext/eventsub/websocket.py | 4 ++-- twitchio/http.py | 2 +- twitchio/models.py | 29 ++++++++++++++++++++--------- twitchio/user.py | 7 ++++--- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/twitchio/ext/eventsub/websocket.py b/twitchio/ext/eventsub/websocket.py index 4c3d889f..cc0ce219 100644 --- a/twitchio/ext/eventsub/websocket.py +++ b/twitchio/ext/eventsub/websocket.py @@ -209,10 +209,10 @@ async def pump(self) -> None: await self.connect() return - except TypeError as e: + except TypeError as e: logger.warning(f"Received bad frame: {e.args[0]}") - if e.args[0] is None: # websocket was closed, reconnect + if e.args[0] is None: # websocket was closed, reconnect logger.info("Known bad frame, restarting connection") await self.connect() return diff --git a/twitchio/http.py b/twitchio/http.py index ec713081..104d8d6d 100644 --- a/twitchio/http.py +++ b/twitchio/http.py @@ -709,7 +709,7 @@ async def get_user_emotes(self, user_id: str, broadcaster_id: Optional[str], tok q: List = [("user_id", user_id)] if broadcaster_id: q.append(("broadcaster_id", broadcaster_id)) - + return await self.request(Route("GET", "chat/emotes/user", query=q, token=token)) async def get_stream_key(self, token: str, broadcaster_id: str): diff --git a/twitchio/models.py b/twitchio/models.py index 483f20ea..80c98797 100644 --- a/twitchio/models.py +++ b/twitchio/models.py @@ -659,7 +659,7 @@ class Emote: Represents an Emote. .. note:: - + It seems twitch is sometimes returning duplicate information from the emotes endpoint. To deduplicate your emotes, you can call ``set()`` on the list of emotes (or any other hashmap), which will remove the duplicates. @@ -667,7 +667,7 @@ class Emote: my_list_of_emotes = await user.get_user_emotes(...) deduplicated_emotes = set(my_list_of_emotes) - + Attributes ----------- id: :class:`str` @@ -701,7 +701,18 @@ class Emote: Whether this emote is available in dark theme background mode. """ - __slots__ = "id", "set_id", "owner_id", "name", "type", "scales", "format_static", "format_animated", "theme_light", "theme_dark" + __slots__ = ( + "id", + "set_id", + "owner_id", + "name", + "type", + "scales", + "format_static", + "format_animated", + "theme_light", + "theme_dark", + ) def __init__(self, data: dict) -> None: self.id: str = data["id"] @@ -714,7 +725,7 @@ def __init__(self, data: dict) -> None: self.theme_light: bool = "light" in data["theme_mode"] self.format_static: bool = "static" in data["format"] self.format_animated: bool = "animated" in data["format"] - + def url_for(self, format: Literal["static", "animated"], theme: Literal["dark", "light"], scale: str) -> str: """ Returns a cdn url that can be used to download or serve the emote on a website. @@ -729,26 +740,26 @@ def url_for(self, format: Literal["static", "animated"], theme: Literal["dark", scale: :class:`str` The scale of the emote. This should be formatted in this format: ``"1.0"``. The scales available for this emote can be checked via :attr:`~.scales`. - + Returns -------- :class:`str` """ if scale not in self.scales: raise ValueError(f"scale for this emote must be one of {', '.join(self.scales)}, not {scale}") - + if (theme == "dark" and not self.theme_dark) or (theme == "light" and not self.theme_light): raise ValueError(f"theme {theme} is not an available value for this emote") if (format == "static" and not self.format_static) or (format == "animated" and not self.format_animated): raise ValueError(f"format {format} is not an available value for this emote") - + return f"https://static-cdn.jtvnw.net/emoticons/v2/{self.id}/{format}/{theme}/{scale}" - + def __repr__(self) -> str: return f"" - def __hash__(self) -> int: # this exists so we can do set(list of emotes) to get rid of duplicates + def __hash__(self) -> int: # this exists so we can do set(list of emotes) to get rid of duplicates return hash(self.id) diff --git a/twitchio/user.py b/twitchio/user.py index 14595398..d0ab62f3 100644 --- a/twitchio/user.py +++ b/twitchio/user.py @@ -639,17 +639,17 @@ async def fetch_channel_emotes(self): data = await self._http.get_channel_emotes(str(self.id)) return [ChannelEmote(self._http, x) for x in data] - + async def fetch_user_emotes(self, token: str, broadcaster: Optional[PartialUser] = None) -> List[Emote]: """|coro| - + Fetches emotes the user has access to. Optionally, you can filter by a broadcaster. .. note:: As of writing, this endpoint seems extrememly unoptimized by twitch, and may (read: will) take a lot of API requests to load. See https://github.com/twitchdev/issues/issues/921 . - + Parameters ----------- token: :class:`str` @@ -663,6 +663,7 @@ async def fetch_user_emotes(self, token: str, broadcaster: Optional[PartialUser] List[:class:`~twitchio.Emote`] """ from .models import Emote + data = await self._http.get_user_emotes(str(self.id), broadcaster and str(broadcaster.id), token) return [Emote(d) for d in data] From d1687fc009a2e0d7010771c7e417f8a330ce7912 Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Wed, 20 Mar 2024 22:13:28 -0700 Subject: [PATCH 12/36] actually restart on bad frame --- docs/conf.py | 2 +- twitchio/__init__.py | 4 ++-- twitchio/ext/eventsub/websocket.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 10f566cc..fbe11f6b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -26,7 +26,7 @@ on_rtd = os.environ.get("READTHEDOCS") == "True" project = "TwitchIO" -copyright = "2023, TwitchIO" +copyright = "2024, TwitchIO" author = "PythonistaGuild" # The full version, including alpha/beta/rc tags diff --git a/twitchio/__init__.py b/twitchio/__init__.py index 699f347a..30fe92e2 100644 --- a/twitchio/__init__.py +++ b/twitchio/__init__.py @@ -27,8 +27,8 @@ __title__ = "TwitchIO" __author__ = "TwitchIO, PythonistaGuild" __license__ = "MIT" -__copyright__ = "Copyright 2017-2022 (c) TwitchIO" -__version__ = "2.8.2" +__copyright__ = "Copyright 2017-present (c) TwitchIO" +__version__ = "2.9.1" from .client import Client from .user import * diff --git a/twitchio/ext/eventsub/websocket.py b/twitchio/ext/eventsub/websocket.py index cc0ce219..a4309729 100644 --- a/twitchio/ext/eventsub/websocket.py +++ b/twitchio/ext/eventsub/websocket.py @@ -212,7 +212,7 @@ async def pump(self) -> None: except TypeError as e: logger.warning(f"Received bad frame: {e.args[0]}") - if e.args[0] is None: # websocket was closed, reconnect + if "257" in e.args[0]: # websocket was closed, reconnect logger.info("Known bad frame, restarting connection") await self.connect() return From 49fe6d0fda0b1b21bb7954e29833975eaca171f2 Mon Sep 17 00:00:00 2001 From: Tom Date: Thu, 21 Mar 2024 09:45:47 -0700 Subject: [PATCH 13/36] Update changelog.rst --- docs/changelog.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index c437f787..5eac8bf4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,11 @@ :orphan: +2.9.1 +======= +- ext.eventsub + - Bug fixes + - fix: Special-cased a restart when a specific known bad frame is received. + 2.9.0 ======= - TwitchIO From 9e6ecc9a2ccdef71ffa0e777486b9a2f71834fb4 Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Mon, 6 May 2024 22:20:15 -0700 Subject: [PATCH 14/36] add ban request eventsub endpoints --- docs/changelog.rst | 9 ++++ twitchio/ext/eventsub/models.py | 73 ++++++++++++++++++++++++++++++ twitchio/ext/eventsub/server.py | 6 +++ twitchio/ext/eventsub/websocket.py | 6 +++ 4 files changed, 94 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 97d39f38..738e610c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,5 +1,14 @@ :orphan: +2.10.0 +======= +- ext.eventsub + - Additions + - Added :method:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_unban_request_create ` / + :method:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_unban_request_create ` + - Added :method:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_unban_request_resolve ` / + :method:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_unban_request_resolve ` + 2.9.2 ======= - TwitchIO diff --git a/twitchio/ext/eventsub/models.py b/twitchio/ext/eventsub/models.py index 8e22334a..9c4e60ee 100644 --- a/twitchio/ext/eventsub/models.py +++ b/twitchio/ext/eventsub/models.py @@ -1619,6 +1619,74 @@ def __init__(self, client: EventSubClient, data: dict): self.donation_decimal_places: int = data["amount"]["decimal_places"] +class ChannelUnbanRequestCreate(EventData): + """ + Represents an unban request created by a user. + + Attributes + ----------- + id: :class:`str` + The ID of the ban request. + broadcaster: :class:`PartialUser` + The broadcaster from which the user was banned. + user: :class:`PartialUser` + The user that was banned. + text: :class:`str` + The unban request text the user submitted. + created_at: :class:`datetime.datetime` + When the user submitted the request. + """ + + __slots__ = ( + "id", + "broadcaster", + "user", + "text", + "created_at" + ) + + def __init__(self, client: EventSubClient, data: dict): + self.id: str = data["id"] + self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster") + self.user: PartialUser = _transform_user(client, data, "user") + self.text: str = data["text"] + self.created_at: datetime.datetime = _parse_datetime(data["created_at"]) + +class ChannelUnbanRequestResolve(EventData): + """ + Represents an unban request that has been resolved by a moderator. + + Attributes + ----------- + id: :class:`str` + The ID of the ban request. + broadcaster: :class:`PartialUser` + The broadcaster from which the user was banned. + user: :class:`PartialUser` + The user that was banned. + moderator: :class:`PartialUser` + The moderator that handled this unban request. + resolution_text: :class:`str` + The reasoning provided by the moderator. + status: :class:`str` + The resolution. either `accepted` or `denied`. + """ + + __slots__ = ( + "id", + "broadcaster", + "user", + "text", + "created_at" + ) + + def __init__(self, client: EventSubClient, data: dict): + self.id: str = data["id"] + self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster") + self.user: PartialUser = _transform_user(client, data, "user") + self.text: str = data["text"] + self.created_at: datetime.datetime = _parse_datetime(data["created_at"]) + _DataType = Union[ ChannelBanData, ChannelUnbanData, @@ -1652,6 +1720,8 @@ def __init__(self, client: EventSubClient, data: dict): ChannelShoutoutCreateData, ChannelShoutoutReceiveData, ChannelCharityDonationData, + ChannelUnbanRequestCreate, + ChannelUnbanRequestResolve ] @@ -1722,6 +1792,9 @@ class _SubscriptionTypes(metaclass=_SubTypesMeta): stream_start = "stream.online", 1, StreamOnlineData stream_end = "stream.offline", 1, StreamOfflineData + unban_request_create = "channel.unban_request.create", 1, ChannelUnbanRequestCreate + unban_request_resolve = "channel.unban_request.resolve", 1, ChannelUnbanRequestResolve + user_authorization_grant = "user.authorization.grant", 1, UserAuthorizationGrantedData user_authorization_revoke = "user.authorization.revoke", 1, UserAuthorizationRevokedData diff --git a/twitchio/ext/eventsub/server.py b/twitchio/ext/eventsub/server.py index 3a061bde..c7d09420 100644 --- a/twitchio/ext/eventsub/server.py +++ b/twitchio/ext/eventsub/server.py @@ -275,6 +275,12 @@ def subscribe_channel_shoutout_receive( return self._subscribe_with_broadcaster_moderator( models.SubscriptionTypes.channel_shoutout_receive, broadcaster, moderator ) + + def subscribe_channel_unban_request_create(self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int]): + return self._subscribe_with_broadcaster_moderator(models.SubscriptionTypes.unban_request_create, broadcaster, moderator) + + def subscribe_channel_unban_request_resolve(self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int]): + return self._subscribe_with_broadcaster_moderator(models.SubscriptionTypes.unban_request_resolve, broadcaster, moderator) def subscribe_channel_charity_donate(self, broadcaster: Union[PartialUser, str, int]): return self._subscribe_with_broadcaster(models.SubscriptionTypes.channel_charity_donate, broadcaster) diff --git a/twitchio/ext/eventsub/websocket.py b/twitchio/ext/eventsub/websocket.py index a4309729..87116e18 100644 --- a/twitchio/ext/eventsub/websocket.py +++ b/twitchio/ext/eventsub/websocket.py @@ -513,3 +513,9 @@ async def subscribe_channel_shoutout_receive( async def subscribe_channel_charity_donate(self, broadcaster: Union[PartialUser, str, int], token: str): await self._subscribe_with_broadcaster(models.SubscriptionTypes.channel_charity_donate, broadcaster, token) + + async def subscribe_channel_unban_request_create(self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int], token: str): + await self._subscribe_with_broadcaster_moderator(models.SubscriptionTypes.unban_request_create, broadcaster, moderator, token) + + async def subscribe_channel_unban_request_resolve(self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int], token: str): + await self._subscribe_with_broadcaster_moderator(models.SubscriptionTypes.unban_request_resolve, broadcaster, moderator, token) From 4140213f33338628262beac7a1d526dbcd889526 Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Mon, 6 May 2024 22:23:01 -0700 Subject: [PATCH 15/36] fix poll choice model since twitch removed the key --- twitchio/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twitchio/models.py b/twitchio/models.py index 80c98797..422b1581 100644 --- a/twitchio/models.py +++ b/twitchio/models.py @@ -1802,7 +1802,7 @@ def __init__(self, data: dict): self.title: str = data["title"] self.votes: int = data["votes"] self.channel_points_votes: int = data["channel_points_votes"] - self.bits_votes: int = data["bits_votes"] + self.bits_votes: int = 0 def __repr__(self): return f"" From 8adb685bfa37c76bdf4f73a00cd56e0f8facc7d7 Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Sun, 12 May 2024 20:52:01 -0700 Subject: [PATCH 16/36] run lints --- twitchio/ext/eventsub/models.py | 20 +++++--------------- twitchio/ext/eventsub/server.py | 20 ++++++++++++++------ twitchio/ext/eventsub/websocket.py | 18 +++++++++++++----- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/twitchio/ext/eventsub/models.py b/twitchio/ext/eventsub/models.py index 9c4e60ee..75739813 100644 --- a/twitchio/ext/eventsub/models.py +++ b/twitchio/ext/eventsub/models.py @@ -1637,13 +1637,7 @@ class ChannelUnbanRequestCreate(EventData): When the user submitted the request. """ - __slots__ = ( - "id", - "broadcaster", - "user", - "text", - "created_at" - ) + __slots__ = ("id", "broadcaster", "user", "text", "created_at") def __init__(self, client: EventSubClient, data: dict): self.id: str = data["id"] @@ -1652,6 +1646,7 @@ def __init__(self, client: EventSubClient, data: dict): self.text: str = data["text"] self.created_at: datetime.datetime = _parse_datetime(data["created_at"]) + class ChannelUnbanRequestResolve(EventData): """ Represents an unban request that has been resolved by a moderator. @@ -1672,13 +1667,7 @@ class ChannelUnbanRequestResolve(EventData): The resolution. either `accepted` or `denied`. """ - __slots__ = ( - "id", - "broadcaster", - "user", - "text", - "created_at" - ) + __slots__ = ("id", "broadcaster", "user", "text", "created_at") def __init__(self, client: EventSubClient, data: dict): self.id: str = data["id"] @@ -1687,6 +1676,7 @@ def __init__(self, client: EventSubClient, data: dict): self.text: str = data["text"] self.created_at: datetime.datetime = _parse_datetime(data["created_at"]) + _DataType = Union[ ChannelBanData, ChannelUnbanData, @@ -1721,7 +1711,7 @@ def __init__(self, client: EventSubClient, data: dict): ChannelShoutoutReceiveData, ChannelCharityDonationData, ChannelUnbanRequestCreate, - ChannelUnbanRequestResolve + ChannelUnbanRequestResolve, ] diff --git a/twitchio/ext/eventsub/server.py b/twitchio/ext/eventsub/server.py index c7d09420..2d9fe2cd 100644 --- a/twitchio/ext/eventsub/server.py +++ b/twitchio/ext/eventsub/server.py @@ -275,12 +275,20 @@ def subscribe_channel_shoutout_receive( return self._subscribe_with_broadcaster_moderator( models.SubscriptionTypes.channel_shoutout_receive, broadcaster, moderator ) - - def subscribe_channel_unban_request_create(self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int]): - return self._subscribe_with_broadcaster_moderator(models.SubscriptionTypes.unban_request_create, broadcaster, moderator) - - def subscribe_channel_unban_request_resolve(self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int]): - return self._subscribe_with_broadcaster_moderator(models.SubscriptionTypes.unban_request_resolve, broadcaster, moderator) + + def subscribe_channel_unban_request_create( + self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int] + ): + return self._subscribe_with_broadcaster_moderator( + models.SubscriptionTypes.unban_request_create, broadcaster, moderator + ) + + def subscribe_channel_unban_request_resolve( + self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int] + ): + return self._subscribe_with_broadcaster_moderator( + models.SubscriptionTypes.unban_request_resolve, broadcaster, moderator + ) def subscribe_channel_charity_donate(self, broadcaster: Union[PartialUser, str, int]): return self._subscribe_with_broadcaster(models.SubscriptionTypes.channel_charity_donate, broadcaster) diff --git a/twitchio/ext/eventsub/websocket.py b/twitchio/ext/eventsub/websocket.py index 87116e18..1acf1268 100644 --- a/twitchio/ext/eventsub/websocket.py +++ b/twitchio/ext/eventsub/websocket.py @@ -514,8 +514,16 @@ async def subscribe_channel_shoutout_receive( async def subscribe_channel_charity_donate(self, broadcaster: Union[PartialUser, str, int], token: str): await self._subscribe_with_broadcaster(models.SubscriptionTypes.channel_charity_donate, broadcaster, token) - async def subscribe_channel_unban_request_create(self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int], token: str): - await self._subscribe_with_broadcaster_moderator(models.SubscriptionTypes.unban_request_create, broadcaster, moderator, token) - - async def subscribe_channel_unban_request_resolve(self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int], token: str): - await self._subscribe_with_broadcaster_moderator(models.SubscriptionTypes.unban_request_resolve, broadcaster, moderator, token) + async def subscribe_channel_unban_request_create( + self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int], token: str + ): + await self._subscribe_with_broadcaster_moderator( + models.SubscriptionTypes.unban_request_create, broadcaster, moderator, token + ) + + async def subscribe_channel_unban_request_resolve( + self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int], token: str + ): + await self._subscribe_with_broadcaster_moderator( + models.SubscriptionTypes.unban_request_resolve, broadcaster, moderator, token + ) From b3d19616d83274ea1cdabddfefebc5ea96fe6020 Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Sun, 12 May 2024 20:56:29 -0700 Subject: [PATCH 17/36] Use data suffix to stay in line with other models --- twitchio/ext/eventsub/models.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/twitchio/ext/eventsub/models.py b/twitchio/ext/eventsub/models.py index 75739813..c68c6fff 100644 --- a/twitchio/ext/eventsub/models.py +++ b/twitchio/ext/eventsub/models.py @@ -1619,7 +1619,7 @@ def __init__(self, client: EventSubClient, data: dict): self.donation_decimal_places: int = data["amount"]["decimal_places"] -class ChannelUnbanRequestCreate(EventData): +class ChannelUnbanRequestCreateData(EventData): """ Represents an unban request created by a user. @@ -1647,7 +1647,7 @@ def __init__(self, client: EventSubClient, data: dict): self.created_at: datetime.datetime = _parse_datetime(data["created_at"]) -class ChannelUnbanRequestResolve(EventData): +class ChannelUnbanRequestResolveData(EventData): """ Represents an unban request that has been resolved by a moderator. @@ -1710,8 +1710,8 @@ def __init__(self, client: EventSubClient, data: dict): ChannelShoutoutCreateData, ChannelShoutoutReceiveData, ChannelCharityDonationData, - ChannelUnbanRequestCreate, - ChannelUnbanRequestResolve, + ChannelUnbanRequestCreateData, + ChannelUnbanRequestResolveData, ] @@ -1782,8 +1782,8 @@ class _SubscriptionTypes(metaclass=_SubTypesMeta): stream_start = "stream.online", 1, StreamOnlineData stream_end = "stream.offline", 1, StreamOfflineData - unban_request_create = "channel.unban_request.create", 1, ChannelUnbanRequestCreate - unban_request_resolve = "channel.unban_request.resolve", 1, ChannelUnbanRequestResolve + unban_request_create = "channel.unban_request.create", 1, ChannelUnbanRequestCreateData + unban_request_resolve = "channel.unban_request.resolve", 1, ChannelUnbanRequestResolveData user_authorization_grant = "user.authorization.grant", 1, UserAuthorizationGrantedData user_authorization_revoke = "user.authorization.revoke", 1, UserAuthorizationRevokedData From fc853aaebb1bd57ac6ba1d0af911865873b6464e Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Sun, 12 May 2024 22:25:31 -0700 Subject: [PATCH 18/36] implement eventsub automod --- twitchio/ext/eventsub/models.py | 219 ++++++++++++++++++++++++++++- twitchio/ext/eventsub/server.py | 30 +++- twitchio/ext/eventsub/websocket.py | 28 ++++ 3 files changed, 275 insertions(+), 2 deletions(-) diff --git a/twitchio/ext/eventsub/models.py b/twitchio/ext/eventsub/models.py index c68c6fff..3a30751c 100644 --- a/twitchio/ext/eventsub/models.py +++ b/twitchio/ext/eventsub/models.py @@ -4,7 +4,7 @@ import hashlib import logging from enum import Enum -from typing import Dict, TYPE_CHECKING, Optional, Type, Union, Tuple, List, overload +from typing import Any, Dict, TYPE_CHECKING, Optional, Type, Union, Tuple, List, overload from typing_extensions import Literal from aiohttp import web @@ -1677,6 +1677,214 @@ def __init__(self, client: EventSubClient, data: dict): self.created_at: datetime.datetime = _parse_datetime(data["created_at"]) +class AutomodMessageHoldData(EventData): + """ + Represents a message being held by automod for manual review. + + Attributes + ------------ + message_id: :class:`str` + The ID of the message. + message_content: :class:`str` + The contents of the message + broadcaster: :class:`PartialUser` + The broadcaster from which the message was held. + user: :class:`PartialUser` + The user that sent the message. + level: :class:`int` + The level of alarm raised for this message. + category: :class:`str` + The category of alarm that was raised for this message. + created_at: :class:`datetime.datetime` + When this message was held. + message_fragments: :class:`dict` + The fragments of this message. This includes things such as emotes and cheermotes. An example from twitch is provided: + + .. code:: json + + { + "emotes": [ + { + "text": "badtextemote1", + "id": "emote-123", + "set-id": "set-emote-1" + }, + { + "text": "badtextemote2", + "id": "emote-234", + "set-id": "set-emote-2" + } + ], + "cheermotes": [ + { + "text": "badtextcheermote1", + "amount": 1000, + "prefix": "prefix", + "tier": 1 + } + ] + } + """ + + __slots__ = ("message_id", "message_content", "broadcaster", "user", "level", "category", "message_fragments", "created_at") + + def __init__(self, client: EventSubClient, data: dict): + self.message_id: str = data["message_id"] + self.message_content: str = data["message"] + self.message_fragments: Dict[str, Dict[str, Any]] = data["fragments"] + self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster_user") + self.user: PartialUser = _transform_user(client, data, "user") + self.level: int = data["level"] + self.category: str = data["category"] + self.created_at: datetime.datetime = _parse_datetime(data["held_at"]) + + +class AutomodMessageUpdateData(EventData): + """ + Represents a message that was updated by a moderator in the automod queue. + + Attributes + ------------ + message_id: :class:`str` + The ID of the message. + message_content: :class:`str` + The contents of the message + broadcaster: :class:`PartialUser` + The broadcaster from which the message was held. + user: :class:`PartialUser` + The user that sent the message. + moderator: :class:`PartialUser` + The moderator that updated the message status. + status: :class:`str` + The new status of the message. Typically one of ``approved`` or ``denied``. + level: :class:`int` + The level of alarm raised for this message. + category: :class:`str` + The category of alarm that was raised for this message. + created_at: :class:`datetime.datetime` + When this message was held. + message_fragments: :class:`dict` + The fragments of this message. This includes things such as emotes and cheermotes. An example from twitch is provided: + + .. code:: json + + { + "emotes": [ + { + "text": "badtextemote1", + "id": "emote-123", + "set-id": "set-emote-1" + }, + { + "text": "badtextemote2", + "id": "emote-234", + "set-id": "set-emote-2" + } + ], + "cheermotes": [ + { + "text": "badtextcheermote1", + "amount": 1000, + "prefix": "prefix", + "tier": 1 + } + ] + } + """ + + __slots__ = ("message_id", "message_content", "broadcaster", "user", "moderator", "level", "category", "message_fragments", "created_at", "status") + + def __init__(self, client: EventSubClient, data: dict): + self.message_id: str = data["message_id"] + self.message_content: str = data["message"] + self.message_fragments: Dict[str, Dict[str, Any]] = data["fragments"] + self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster_user") + self.moderator: PartialUser = _transform_user(client, data, "moderator_user") + self.user: PartialUser = _transform_user(client, data, "user") + self.level: int = data["level"] + self.category: str = data["category"] + self.created_at: datetime.datetime = _parse_datetime(data["held_at"]) + self.status: str = data["status"] + + +class AutomodSettingsUpdateData(EventData): + """ + Represents a channels automod settings being updated. + + Attributes + ------------ + broadcaster: :class:`PartialUser` + The broadcaster for which the settings were updated. + moderator: :class:`PartialUser` + The moderator that updated the settings. + overall :class:`int` | ``None`` + The overall level of automod aggressiveness. + disability: :class:`int` | ``None`` + The aggression towards disability. + aggression: :class:`int` | ``None`` + The aggression towards aggressive users. + sex: :class:`int` | ``None`` + The aggression towards sexuality/gender. + misogyny: :class:`int` | ``None`` + The aggression towards misogyny. + bullying: :class:`int` | ``None`` + The aggression towards bullying. + swearing: :class:`int` | ``None`` + The aggression towards cursing/language. + race_religion: :class:`int` | ``None`` + The aggression towards race, ethnicity, and religion. + sexual_terms: :class:`int` | ``None`` + The aggression towards sexual terms/references. + """ + + __slots__ = ("broadcaster", "moderator", "overall", "disability", "aggression", "sex", "misogyny", "bullying", "swearing", "race_religion", "sexual_terms") + + def __init__(self, client: EventSubClient, data: dict): + self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster_user") + self.moderator: PartialUser = _transform_user(client, data, "moderator_user") + self.overall: Optional[int] = data["overall"] + self.disability: Optional[int] = data["disability"] + self.aggression: Optional[int] = data["aggression"] + self.sex: Optional[int] = data["sex"] + self.misogyny: Optional[int] = data["misogyny"] + self.bullying: Optional[int] = data["bullying"] + self.swearing: Optional[int] = data["swearing"] + self.race_religion: Optional[int] = data["race_ethnicity_or_religion"] + self.sexual_terms: Optional[int] = data["sex_based_terms"] + + +class AutomodTermsUpdateData(EventData): + """ + Represents a channels automod terms being updated. + + .. note:: + + Private terms are not sent. + + Attributes + ----------- + broadcaster: :class:`PartialUser` + The broadcaster for which the terms were updated. + moderator: :class:`PartialUser` + The moderator who updated the terms. + action: :class:`str` + The action type. + from_automod: :class:`bool` + Whether the action was taken by automod. + terms: List[:class:`str`] + The terms that were applied. + """ + + __slots__ = ("broadcaster", "moderator", "action", "from_automod", "terms") + + def __init__(self, client: EventSubClient, data: dict): + self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster_user") + self.moderator: PartialUser = _transform_user(client, data, "moderator_user") + self.action: str = data["action"] + self.from_automod: bool = data["from_automod"] + self.terms: List[str] = data["terms"] + + _DataType = Union[ ChannelBanData, ChannelUnbanData, @@ -1712,6 +1920,10 @@ def __init__(self, client: EventSubClient, data: dict): ChannelCharityDonationData, ChannelUnbanRequestCreateData, ChannelUnbanRequestResolveData, + AutomodMessageHoldData, + AutomodMessageUpdateData, + AutomodSettingsUpdateData, + AutomodTermsUpdateData ] @@ -1726,6 +1938,11 @@ class _SubscriptionTypes(metaclass=_SubTypesMeta): _type_map: Dict[str, Type[_DataType]] _name_map: Dict[str, str] + automod_message_hold = "automod.message.hold", 1, AutomodMessageHoldData + automod_message_update = "automod.message.update", 1, AutomodMessageUpdateData + automod_settings_update = "automod.settings.update", 1, AutomodSettingsUpdateData + automod_terms_update = "automod.terms.update", 1, AutomodTermsUpdateData + follow = "channel.follow", 1, ChannelFollowData followV2 = "channel.follow", 2, ChannelFollowData subscription = "channel.subscribe", 1, ChannelSubscribeData diff --git a/twitchio/ext/eventsub/server.py b/twitchio/ext/eventsub/server.py index 2d9fe2cd..38a0745e 100644 --- a/twitchio/ext/eventsub/server.py +++ b/twitchio/ext/eventsub/server.py @@ -289,7 +289,35 @@ def subscribe_channel_unban_request_resolve( return self._subscribe_with_broadcaster_moderator( models.SubscriptionTypes.unban_request_resolve, broadcaster, moderator ) - + + def subscribe_automod_message_hold( + self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int] + ): + return self._subscribe_with_broadcaster_moderator( + models.SubscriptionTypes.automod_message_hold, broadcaster, moderator + ) + + def subscribe_automod_message_update( + self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int] + ): + return self._subscribe_with_broadcaster_moderator( + models.SubscriptionTypes.automod_message_update, broadcaster, moderator + ) + + def subscribe_automod_settings_update( + self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int] + ): + return self._subscribe_with_broadcaster_moderator( + models.SubscriptionTypes.automod_settings_update, broadcaster, moderator + ) + + def subscribe_automod_terms_update( + self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int] + ): + return self._subscribe_with_broadcaster_moderator( + models.SubscriptionTypes.automod_terms_update, broadcaster, moderator + ) + def subscribe_channel_charity_donate(self, broadcaster: Union[PartialUser, str, int]): return self._subscribe_with_broadcaster(models.SubscriptionTypes.channel_charity_donate, broadcaster) diff --git a/twitchio/ext/eventsub/websocket.py b/twitchio/ext/eventsub/websocket.py index 1acf1268..3dc84aa8 100644 --- a/twitchio/ext/eventsub/websocket.py +++ b/twitchio/ext/eventsub/websocket.py @@ -527,3 +527,31 @@ async def subscribe_channel_unban_request_resolve( await self._subscribe_with_broadcaster_moderator( models.SubscriptionTypes.unban_request_resolve, broadcaster, moderator, token ) + + async def subscribe_automod_message_hold( + self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int], token: str + ): + await self._subscribe_with_broadcaster_moderator( + models.SubscriptionTypes.automod_message_hold, broadcaster, moderator, token + ) + + async def subscribe_automod_message_update( + self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int], token: str + ): + await self._subscribe_with_broadcaster_moderator( + models.SubscriptionTypes.automod_message_update, broadcaster, moderator, token + ) + + async def subscribe_automod_settings_update( + self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int], token: str + ): + await self._subscribe_with_broadcaster_moderator( + models.SubscriptionTypes.automod_settings_update, broadcaster, moderator, token + ) + + async def subscribe_automod_terms_update( + self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int], token: str + ): + await self._subscribe_with_broadcaster_moderator( + models.SubscriptionTypes.automod_terms_update, broadcaster, moderator, token + ) \ No newline at end of file From 03b5c6c891dcd680d66afe520c704133a2b1a9b6 Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Sun, 12 May 2024 22:27:53 -0700 Subject: [PATCH 19/36] document new eventsub models --- docs/exts/eventsub.rst | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/exts/eventsub.rst b/docs/exts/eventsub.rst index 9fac08af..999dcab5 100644 --- a/docs/exts/eventsub.rst +++ b/docs/exts/eventsub.rst @@ -495,3 +495,38 @@ API Reference :members: :inherited-members: +.. attributetable::: ChannelUnbanRequestCreateData + +.. autoclass:: ChannelUnbanRequestCreateData + :members: + :inherited-members: + +.. attributetable::: ChannelUnbanRequestResolveData + +.. autoclass:: ChannelUnbanRequestResolveData + :members: + :inherited-members: + +.. attributetable::: AutomodMessageHoldData + +.. autoclass:: AutomodMessageHoldData + :members: + :inherited-members: + +.. attributetable::: AutomodMessageUpdateData + +.. autoclass:: AutomodMessageUpdateData + :members: + :inherited-members: + +.. attributetable::: AutomodSettingsUpdateData + +.. autoclass:: AutomodSettingsUpdateData + :members: + :inherited-members: + +.. attributetable::: AutomodTermsUpdateData + +.. autoclass:: AutomodTermsUpdateData + :members: + :inherited-members: From ef7467c1fb2ec2eab7023c8931041dcf24685da4 Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Sun, 12 May 2024 22:28:14 -0700 Subject: [PATCH 20/36] run lints --- twitchio/ext/eventsub/models.py | 44 ++++++++++++++++++++++++++---- twitchio/ext/eventsub/server.py | 10 +++---- twitchio/ext/eventsub/websocket.py | 8 +++--- 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/twitchio/ext/eventsub/models.py b/twitchio/ext/eventsub/models.py index 3a30751c..c71f88d8 100644 --- a/twitchio/ext/eventsub/models.py +++ b/twitchio/ext/eventsub/models.py @@ -1726,7 +1726,16 @@ class AutomodMessageHoldData(EventData): } """ - __slots__ = ("message_id", "message_content", "broadcaster", "user", "level", "category", "message_fragments", "created_at") + __slots__ = ( + "message_id", + "message_content", + "broadcaster", + "user", + "level", + "category", + "message_fragments", + "created_at", + ) def __init__(self, client: EventSubClient, data: dict): self.message_id: str = data["message_id"] @@ -1791,8 +1800,19 @@ class AutomodMessageUpdateData(EventData): ] } """ - - __slots__ = ("message_id", "message_content", "broadcaster", "user", "moderator", "level", "category", "message_fragments", "created_at", "status") + + __slots__ = ( + "message_id", + "message_content", + "broadcaster", + "user", + "moderator", + "level", + "category", + "message_fragments", + "created_at", + "status", + ) def __init__(self, client: EventSubClient, data: dict): self.message_id: str = data["message_id"] @@ -1836,8 +1856,20 @@ class AutomodSettingsUpdateData(EventData): sexual_terms: :class:`int` | ``None`` The aggression towards sexual terms/references. """ - - __slots__ = ("broadcaster", "moderator", "overall", "disability", "aggression", "sex", "misogyny", "bullying", "swearing", "race_religion", "sexual_terms") + + __slots__ = ( + "broadcaster", + "moderator", + "overall", + "disability", + "aggression", + "sex", + "misogyny", + "bullying", + "swearing", + "race_religion", + "sexual_terms", + ) def __init__(self, client: EventSubClient, data: dict): self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster_user") @@ -1923,7 +1955,7 @@ def __init__(self, client: EventSubClient, data: dict): AutomodMessageHoldData, AutomodMessageUpdateData, AutomodSettingsUpdateData, - AutomodTermsUpdateData + AutomodTermsUpdateData, ] diff --git a/twitchio/ext/eventsub/server.py b/twitchio/ext/eventsub/server.py index 38a0745e..b78849b7 100644 --- a/twitchio/ext/eventsub/server.py +++ b/twitchio/ext/eventsub/server.py @@ -289,35 +289,35 @@ def subscribe_channel_unban_request_resolve( return self._subscribe_with_broadcaster_moderator( models.SubscriptionTypes.unban_request_resolve, broadcaster, moderator ) - + def subscribe_automod_message_hold( self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int] ): return self._subscribe_with_broadcaster_moderator( models.SubscriptionTypes.automod_message_hold, broadcaster, moderator ) - + def subscribe_automod_message_update( self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int] ): return self._subscribe_with_broadcaster_moderator( models.SubscriptionTypes.automod_message_update, broadcaster, moderator ) - + def subscribe_automod_settings_update( self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int] ): return self._subscribe_with_broadcaster_moderator( models.SubscriptionTypes.automod_settings_update, broadcaster, moderator ) - + def subscribe_automod_terms_update( self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int] ): return self._subscribe_with_broadcaster_moderator( models.SubscriptionTypes.automod_terms_update, broadcaster, moderator ) - + def subscribe_channel_charity_donate(self, broadcaster: Union[PartialUser, str, int]): return self._subscribe_with_broadcaster(models.SubscriptionTypes.channel_charity_donate, broadcaster) diff --git a/twitchio/ext/eventsub/websocket.py b/twitchio/ext/eventsub/websocket.py index 3dc84aa8..5787d420 100644 --- a/twitchio/ext/eventsub/websocket.py +++ b/twitchio/ext/eventsub/websocket.py @@ -534,24 +534,24 @@ async def subscribe_automod_message_hold( await self._subscribe_with_broadcaster_moderator( models.SubscriptionTypes.automod_message_hold, broadcaster, moderator, token ) - + async def subscribe_automod_message_update( self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int], token: str ): await self._subscribe_with_broadcaster_moderator( models.SubscriptionTypes.automod_message_update, broadcaster, moderator, token ) - + async def subscribe_automod_settings_update( self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int], token: str ): await self._subscribe_with_broadcaster_moderator( models.SubscriptionTypes.automod_settings_update, broadcaster, moderator, token ) - + async def subscribe_automod_terms_update( self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int], token: str ): await self._subscribe_with_broadcaster_moderator( models.SubscriptionTypes.automod_terms_update, broadcaster, moderator, token - ) \ No newline at end of file + ) From a5585de3719ef418ec224f2becbb80c07c8b964d Mon Sep 17 00:00:00 2001 From: clevernt Date: Tue, 18 Jun 2024 08:09:44 +0300 Subject: [PATCH 21/36] fix grammar (#446) --- twitchio/ext/commands/bot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/twitchio/ext/commands/bot.py b/twitchio/ext/commands/bot.py index 9eda058c..622ef878 100644 --- a/twitchio/ext/commands/bot.py +++ b/twitchio/ext/commands/bot.py @@ -347,7 +347,7 @@ async def invoke(self, context): await context.command(context) def load_module(self, name: str) -> None: - """Method which loads a module and it's cogs. + """Method which loads a module and its cogs. Parameters ------------ @@ -367,7 +367,7 @@ def load_module(self, name: str) -> None: self._modules[name] = module def unload_module(self, name: str) -> None: - """Method which unloads a module and it's cogs. + """Method which unloads a module and its cogs. Parameters ---------- @@ -402,7 +402,7 @@ def unload_module(self, name: str) -> None: del sys.modules[m] def reload_module(self, name: str): - """Method which reloads a module and it's cogs. + """Method which reloads a module and its cogs. Parameters ---------- From ad580de61c7926fae2a133149bd9103dfe6136b2 Mon Sep 17 00:00:00 2001 From: Zarithya <137598817+Zarithya@users.noreply.github.com> Date: Mon, 17 Jun 2024 22:13:38 -0700 Subject: [PATCH 22/36] Fix fetch_markers passing list instead of dict (#451) * Update user.py The Twitch API describes the "videos" element in the response as "A list of videos that contain markers. The list contains a single video." Currently, that list is being passed when creating the VideoMarkers object, when only the first element of that "list" should be. This fixes this issue that arises when retrieving markers: ``` File "C:\Users\Zari\AppData\Roaming\Python\Python312\site-packages\twitchio\models.py", line 814, in __init__ self.markers = [Marker(d) for d in data["markers"]] ~~~~^^^^^^^^^^^ TypeError: list indices must be integers or slices, not str``` * Update changelog.rst * Update changelog.rst --------- Co-authored-by: Tom --- docs/changelog.rst | 3 +++ twitchio/user.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 738e610c..eea17896 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,9 @@ 2.10.0 ======= +- TwitchIO + - Bug fixes + - fix: :func:`~twitchio.PartialUser.fetch_markers` was passing list of one element from payload, now just passes element - ext.eventsub - Additions - Added :method:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_unban_request_create ` / diff --git a/twitchio/user.py b/twitchio/user.py index 6fb80d75..984ce973 100644 --- a/twitchio/user.py +++ b/twitchio/user.py @@ -770,7 +770,7 @@ async def fetch_markers(self, token: str, video_id: str = None): data = await self._http.get_stream_markers(token, user_id=str(self.id), video_id=video_id) if data: - return VideoMarkers(data[0]["videos"]) + return VideoMarkers(data[0]["videos"][0]) async def fetch_extensions(self, token: str): """|coro| From 9fe0a85bb827677b0d19d3d507542c3635b41c01 Mon Sep 17 00:00:00 2001 From: Aria Taylor <85381807+aricodes-oss@users.noreply.github.com> Date: Tue, 18 Jun 2024 01:16:42 -0400 Subject: [PATCH 23/36] Be more specific in add_command error message (#453) * Be more specific in add_command error message * Update changelog - v2.9.3 --------- Co-authored-by: Tom --- docs/changelog.rst | 12 +++++++++--- twitchio/ext/commands/bot.py | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index eea17896..ad19e8ab 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,10 +1,16 @@ :orphan: + 2.10.0 ======= - TwitchIO - Bug fixes - fix: :func:`~twitchio.PartialUser.fetch_markers` was passing list of one element from payload, now just passes element + +- ext.commands + - Changes + - Added which alias failed to load in the error raised by :func:`~twitchio.ext.commands.Context.add_command` + - ext.eventsub - Additions - Added :method:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_unban_request_create ` / @@ -78,7 +84,7 @@ - New models for the new methods have been added: - :class:`~twitchio.ChannelFollowerEvent` - :class:`~twitchio.ChannelFollowingEvent` - - New optional ``is_featured`` query parameter for :func:`~twitchio.PartialUser.fetch_clips` + - New optional ``is_featured`` query parameter for :func:`~twitchio.PartialUser.fetch_clips` - New attribute :attr:`~twitchio.Clip.is_featured` for :class:`~twitchio.Clip` - Bug fixes @@ -116,14 +122,14 @@ - Added :func:`~twitchio.Client.fetch_content_classification_labels` along with :class:`~twitchio.ContentClassificationLabel` - Added :attr:`~twitchio.ChannelInfo.content_classification_labels` and :attr:`~twitchio.ChannelInfo.is_branded_content` to :class:`~twitchio.ChannelInfo` - Added new parameters to :func:`~twitchio.PartialUser.modify_stream` for ``is_branded_content`` and ``content_classification_labels`` - + - Bug fixes - Fix :func:`~twitchio.Client.search_categories` due to :attr:`~twitchio.Game.igdb_id` being added to :class:`~twitchio.Game` - Made Chatter :attr:`~twitchio.Chatter.id` property public - :func:`~twitchio.Client.event_token_expired` will now be called correctly when response is ``401 Invalid OAuth token`` - Fix reconnect loop when Twitch sends a RECONNECT via IRC websocket - Fix :func:`~twitchio.CustomReward.edit` so it now can enable the reward - + - Other Changes - Updated the HTTPException to provide useful information when an error is raised. diff --git a/twitchio/ext/commands/bot.py b/twitchio/ext/commands/bot.py index 622ef878..942839d4 100644 --- a/twitchio/ext/commands/bot.py +++ b/twitchio/ext/commands/bot.py @@ -209,7 +209,7 @@ def add_command(self, command: Command): if alias in self.commands: del self.commands[command.name] raise TwitchCommandError( - f"Failed to load command <{command.name}>, a command with that name/alias already exists." + f"Failed to load alias <{alias}> for command <{command.name}>, a command with that name/alias already exists.", ) self._command_aliases[alias] = command.name From 2405ddd482a6892ddb68cbab1a6f1802f7b80fb1 Mon Sep 17 00:00:00 2001 From: Ryan <39883642+sockheadrps@users.noreply.github.com> Date: Sat, 22 Jun 2024 03:34:19 -0400 Subject: [PATCH 24/36] Added new dependency for resolving stereo channels and sample rate from audio files. Added setters for rate and channel to expose them incase the meta data is incorrect. (#454) --- docs/changelog.rst | 6 ++++++ docs/requirements.txt | 3 ++- setup.py | 1 + twitchio/ext/sounds/__init__.py | 18 +++++++++++++++--- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ad19e8ab..ba19f7fe 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -17,6 +17,12 @@ :method:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_unban_request_create ` - Added :method:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_unban_request_resolve ` / :method:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_unban_request_resolve ` +- ext.sounds + - Additions + - Added TinyTag as a dependency to support retrieving audio metadata using TinyTag in `ext.sounds.__init__.py`. + - added :method:`Twitchio.ext.sounds.rate setter. + - added :method:`Twitchio.ext.sounds.channels setter. + 2.9.2 ======= diff --git a/docs/requirements.txt b/docs/requirements.txt index 0e5db09c..40eb98e7 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -7,4 +7,5 @@ sphinxext-opengraph Pygments furo pyaudio==0.2.11 -yt-dlp>=2022.2.4 \ No newline at end of file +yt-dlp>=2022.2.4 +tinytag>=1.9.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 3e706895..3c0f4d5d 100644 --- a/setup.py +++ b/setup.py @@ -51,6 +51,7 @@ sounds = [ "yt-dlp>=2022.2.4", 'pyaudio==0.2.11; platform_system!="Windows"', + 'tinytag>=1.9.0', ] speed = [ "ujson>=5.2,<6", diff --git a/twitchio/ext/sounds/__init__.py b/twitchio/ext/sounds/__init__.py index 54989d12..24ff1b87 100644 --- a/twitchio/ext/sounds/__init__.py +++ b/twitchio/ext/sounds/__init__.py @@ -20,6 +20,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + import asyncio import audioop import dataclasses @@ -33,6 +34,7 @@ import pyaudio from yt_dlp import YoutubeDL +from tinytag import TinyTag __all__ = ("Sound", "AudioPlayer") @@ -173,6 +175,9 @@ def __init__( elif isinstance(source, str): self.title = source + tag = TinyTag.get(source) + self._rate = tag.samplerate + self._channels = tag.channels self.proc = subprocess.Popen( [ @@ -189,9 +194,6 @@ def __init__( stdout=subprocess.PIPE, ) - self._channels = 2 - self._rate = 48000 - @classmethod async def ytdl_search(cls, search: str, *, loop: Optional[asyncio.BaseEventLoop] = None): """|coro| @@ -216,11 +218,21 @@ def channels(self): """The audio source channels.""" return self._channels + @channels.setter + def channels(self, channels: int): + """Set audio source channels.""" + self._channels = channels + @property def rate(self): """The audio source sample rate.""" return self._rate + @rate.setter + def rate(self, rate: int): + """Set audio source sample rate.""" + self._rate = rate + @property def source(self): """The raw audio source.""" From b5576c29f0d61bdba2a507f030a71b6a382a0405 Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Sat, 22 Jun 2024 00:58:45 -0700 Subject: [PATCH 25/36] fix string parser --- docs/changelog.rst | 12 ++++++++ twitchio/ext/commands/stringparser.py | 40 ++++++++++++--------------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index ba19f7fe..5d89a7bc 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -10,6 +10,9 @@ - ext.commands - Changes - Added which alias failed to load in the error raised by :func:`~twitchio.ext.commands.Context.add_command` + + - Bug fixes + - fix string parser not properly parsing specific quoted strings - ext.eventsub - Additions @@ -17,6 +20,15 @@ :method:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_unban_request_create ` - Added :method:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_unban_request_resolve ` / :method:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_unban_request_resolve ` + - Added :method:`Twitchio.ext.eventsub.EventSubClient.subscribe_automod_terms_update ` / + :method:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_terms_update ` + - Added :method:`Twitchio.ext.eventsub.EventSubClient.subscribe_automod_settings_update ` / + :method:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_settings_update ` + - Added :method:`Twitchio.ext.eventsub.EventSubClient.subscribe_automod_message_update ` / + :method:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_message_update ` + - Added :method:`Twitchio.ext.eventsub.EventSubClient.subscribe_automod_message_hold ` / + :method:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_message_hold ` + - Added all accompanying models for those endpoints. - ext.sounds - Additions - Added TinyTag as a dependency to support retrieving audio metadata using TinyTag in `ext.sounds.__init__.py`. diff --git a/twitchio/ext/commands/stringparser.py b/twitchio/ext/commands/stringparser.py index 61adabd2..ea3faebd 100644 --- a/twitchio/ext/commands/stringparser.py +++ b/twitchio/ext/commands/stringparser.py @@ -37,35 +37,31 @@ def __init__(self): self.ignore = False def process_string(self, msg: str) -> Dict[int, str]: - while True: - try: - loc = msg[self.count] - except IndexError: - self.eof = self.count - word = msg[self.start : self.eof] - if not word: - break - self.words[self.index] = msg[self.start : self.eof] - break - - if loc.isspace() and not self.ignore: - self.words[self.index] = msg[self.start : self.count].replace(" ", "", 1) + while self.count < len(msg): + loc = msg[self.count] + + if loc == '"' and not self.ignore: + self.ignore = True + self.start = self.count + 1 + + elif loc == '"' and self.ignore: + self.words[self.index] = msg[self.start : self.count] self.index += 1 + self.ignore = False self.start = self.count + 1 - elif loc == '"': - if not self.ignore: - if self.start == self.count: # only tokenize if they're a new word - self.start = self.count + 1 - self.ignore = True - else: + elif loc.isspace() and not self.ignore: + if self.start != self.count: self.words[self.index] = msg[self.start : self.count] self.index += 1 - self.count += 1 - self.start = self.count - self.ignore = False + + self.start = self.count + 1 self.count += 1 + + if self.start < len(msg) and not self.ignore: + self.words[self.index] = msg[self.start : len(msg)].strip() + return self.words def copy(self) -> StringParser: From c1d0dee2ac04b4c2b7e474acaf3cc7c240077e4c Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Sat, 22 Jun 2024 00:59:07 -0700 Subject: [PATCH 26/36] run black --- twitchio/errors.py | 1 + twitchio/ext/commands/errors.py | 1 + twitchio/ext/pubsub/models.py | 1 - twitchio/ext/pubsub/pool.py | 1 + twitchio/ext/pubsub/topics.py | 1 + twitchio/ext/pubsub/websocket.py | 1 + twitchio/user.py | 1 + 7 files changed, 6 insertions(+), 1 deletion(-) diff --git a/twitchio/errors.py b/twitchio/errors.py index 3d0aabab..77e81f82 100644 --- a/twitchio/errors.py +++ b/twitchio/errors.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from typing import Any, Optional diff --git a/twitchio/ext/commands/errors.py b/twitchio/ext/commands/errors.py index eeaa2229..d22d64f2 100644 --- a/twitchio/ext/commands/errors.py +++ b/twitchio/ext/commands/errors.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations from typing import Optional, TYPE_CHECKING diff --git a/twitchio/ext/pubsub/models.py b/twitchio/ext/pubsub/models.py index f11c6f39..0b87c936 100644 --- a/twitchio/ext/pubsub/models.py +++ b/twitchio/ext/pubsub/models.py @@ -22,7 +22,6 @@ DEALINGS IN THE SOFTWARE. """ - from typing import List, Optional from twitchio import PartialUser, Client, Channel, CustomReward, parse_timestamp diff --git a/twitchio/ext/pubsub/pool.py b/twitchio/ext/pubsub/pool.py index 3ce82f3c..1d39e760 100644 --- a/twitchio/ext/pubsub/pool.py +++ b/twitchio/ext/pubsub/pool.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + import copy import itertools import logging diff --git a/twitchio/ext/pubsub/topics.py b/twitchio/ext/pubsub/topics.py index 7fa7e2b4..0a8de4ce 100644 --- a/twitchio/ext/pubsub/topics.py +++ b/twitchio/ext/pubsub/topics.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + import uuid from typing import Optional, List, Type diff --git a/twitchio/ext/pubsub/websocket.py b/twitchio/ext/pubsub/websocket.py index 322e4b58..c8bfcff9 100644 --- a/twitchio/ext/pubsub/websocket.py +++ b/twitchio/ext/pubsub/websocket.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import asyncio diff --git a/twitchio/user.py b/twitchio/user.py index 984ce973..cb1f91f8 100644 --- a/twitchio/user.py +++ b/twitchio/user.py @@ -21,6 +21,7 @@ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + from __future__ import annotations import datetime From 6bcc5a7da13002ac3a21ac4937aeaf0d76f6880c Mon Sep 17 00:00:00 2001 From: chillymosh <86857777+chillymosh@users.noreply.github.com> Date: Sat, 22 Jun 2024 09:28:40 +0100 Subject: [PATCH 27/36] Add SuspiciousUserUpdateData --- twitchio/ext/eventsub/models.py | 64 +++++++++++++++++++++++---------- twitchio/ext/eventsub/server.py | 3 ++ 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/twitchio/ext/eventsub/models.py b/twitchio/ext/eventsub/models.py index c71f88d8..306a43b8 100644 --- a/twitchio/ext/eventsub/models.py +++ b/twitchio/ext/eventsub/models.py @@ -1277,7 +1277,7 @@ class StreamOnlineData(EventData): __slots__ = "broadcaster", "id", "type", "started_at" - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.broadcaster = _transform_user(client, data, "broadcaster_user") self.id: str = data["id"] self.type: Literal["live", "playlist", "watch_party", "premier", "rerun"] = data["type"] @@ -1296,7 +1296,7 @@ class StreamOfflineData(EventData): __slots__ = ("broadcaster",) - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.broadcaster = _transform_user(client, data, "broadcaster_user") @@ -1314,7 +1314,7 @@ class UserAuthorizationGrantedData(EventData): __slots__ = "client_id", "user" - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.user = _transform_user(client, data, "user") self.client_id: str = data["client_id"] @@ -1333,7 +1333,7 @@ class UserAuthorizationRevokedData(EventData): __slots__ = "client_id", "user" - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.user = _transform_user(client, data, "user") self.client_id: str = data["client_id"] @@ -1354,7 +1354,7 @@ class UserUpdateData(EventData): __slots__ = "user", "email", "description" - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.user = _transform_user(client, data, "user") self.email: Optional[str] = data["email"] self.description: str = data["description"] @@ -1384,7 +1384,7 @@ class ChannelGoalBeginProgressData(EventData): __slots__ = "user", "id", "type", "description", "current_amount", "target_amount", "started_at" - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.user = _transform_user(client, data, "broadcaster_user") self.id: str = data["id"] self.type: str = data["type"] @@ -1432,7 +1432,7 @@ class ChannelGoalEndData(EventData): "ended_at", ) - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.user = _transform_user(client, data, "broadcaster_user") self.id: str = data["id"] self.type: str = data["type"] @@ -1460,7 +1460,7 @@ class ChannelShieldModeBeginData(EventData): __slots__ = ("broadcaster", "moderator", "started_at") - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster_user") self.moderator: PartialUser = _transform_user(client, data, "moderator_user") self.started_at: datetime.datetime = _parse_datetime(data["started_at"]) @@ -1482,7 +1482,7 @@ class ChannelShieldModeEndData(EventData): __slots__ = ("broadcaster", "moderator", "ended_at") - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster_user") self.moderator: PartialUser = _transform_user(client, data, "moderator_user") self.ended_at: datetime.datetime = _parse_datetime(data["ended_at"]) @@ -1522,7 +1522,7 @@ class ChannelShoutoutCreateData(EventData): "target_cooldown_ends_at", ) - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster_user") self.moderator: PartialUser = _transform_user(client, data, "moderator_user") self.to_broadcaster: PartialUser = _transform_user(client, data, "to_broadcaster_user") @@ -1552,7 +1552,7 @@ class ChannelShoutoutReceiveData(EventData): __slots__ = ("broadcaster", "from_broadcaster", "started_at", "viewer_count") - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster_user") self.from_broadcaster: PartialUser = _transform_user(client, data, "to_broadcaster_user") self.started_at: datetime.datetime = _parse_datetime(data["started_at"]) @@ -1605,7 +1605,7 @@ class ChannelCharityDonationData(EventData): "donation_currency", ) - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.id: str = data["id"] self.campaign_id: str = data["campaign_id"] self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster") @@ -1639,7 +1639,7 @@ class ChannelUnbanRequestCreateData(EventData): __slots__ = ("id", "broadcaster", "user", "text", "created_at") - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.id: str = data["id"] self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster") self.user: PartialUser = _transform_user(client, data, "user") @@ -1669,7 +1669,7 @@ class ChannelUnbanRequestResolveData(EventData): __slots__ = ("id", "broadcaster", "user", "text", "created_at") - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.id: str = data["id"] self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster") self.user: PartialUser = _transform_user(client, data, "user") @@ -1737,7 +1737,7 @@ class AutomodMessageHoldData(EventData): "created_at", ) - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.message_id: str = data["message_id"] self.message_content: str = data["message"] self.message_fragments: Dict[str, Dict[str, Any]] = data["fragments"] @@ -1814,7 +1814,7 @@ class AutomodMessageUpdateData(EventData): "status", ) - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.message_id: str = data["message_id"] self.message_content: str = data["message"] self.message_fragments: Dict[str, Dict[str, Any]] = data["fragments"] @@ -1871,7 +1871,7 @@ class AutomodSettingsUpdateData(EventData): "sexual_terms", ) - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster_user") self.moderator: PartialUser = _transform_user(client, data, "moderator_user") self.overall: Optional[int] = data["overall"] @@ -1909,13 +1909,38 @@ class AutomodTermsUpdateData(EventData): __slots__ = ("broadcaster", "moderator", "action", "from_automod", "terms") - def __init__(self, client: EventSubClient, data: dict): + def __init__(self, client: EventSubClient, data: dict) -> None: self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster_user") self.moderator: PartialUser = _transform_user(client, data, "moderator_user") self.action: str = data["action"] self.from_automod: bool = data["from_automod"] self.terms: List[str] = data["terms"] +class SuspiciousUserUpdateData(EventData): + """ + Represents a suspicious user update event. + + Attributes + ----------- + broadcaster: :class:`PartialUser` + The channel where the treatment for a suspicious user was updated. + moderator: :class:`PartialUser` + The moderator who updated the terms. + user: :class:`PartialUser` + The the user that sent the message. + trust_status: :class:`Literal["active_monitoring", "restricted", "none"]` + The status set for the suspicious user. Can be the following: “none”, “active_monitoring”, or “restricted”. + """ + + __slots__ = ("broadcaster", "moderator", "user", "trust_status") + + def __init__(self, client: EventSubClient, data: dict) -> None: + self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster_user") + self.moderator: PartialUser = _transform_user(client, data, "moderator_user") + self.user: PartialUser = _transform_user(client, data, "user") + self.trust_status: Literal["active_monitoring", "restricted", "none"] = data["low_trust_status"] + + _DataType = Union[ ChannelBanData, @@ -1956,6 +1981,7 @@ def __init__(self, client: EventSubClient, data: dict): AutomodMessageUpdateData, AutomodSettingsUpdateData, AutomodTermsUpdateData, + SuspiciousUserUpdateData, ] @@ -2038,6 +2064,8 @@ class _SubscriptionTypes(metaclass=_SubTypesMeta): user_authorization_revoke = "user.authorization.revoke", 1, UserAuthorizationRevokedData user_update = "user.update", 1, UserUpdateData + + suspicious_user_update = "channel.suspicious_user.update", 1, SuspiciousUserUpdateData SubscriptionTypes = _SubscriptionTypes() diff --git a/twitchio/ext/eventsub/server.py b/twitchio/ext/eventsub/server.py index b78849b7..0ceca261 100644 --- a/twitchio/ext/eventsub/server.py +++ b/twitchio/ext/eventsub/server.py @@ -321,6 +321,9 @@ def subscribe_automod_terms_update( def subscribe_channel_charity_donate(self, broadcaster: Union[PartialUser, str, int]): return self._subscribe_with_broadcaster(models.SubscriptionTypes.channel_charity_donate, broadcaster) + def subscribe_suspicious_user_update(self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int]): + return self._subscribe_with_broadcaster_moderator(models.SubscriptionTypes.suspicious_user_update, broadcaster, moderator) + async def subscribe_user_authorization_granted(self): return await self._http.create_webhook_subscription( models.SubscriptionTypes.user_authorization_grant, {"client_id": self.client._http.client_id} From 76c01225a0e16bd1a3b430d69ed9a282674d8fcc Mon Sep 17 00:00:00 2001 From: chillymosh <86857777+chillymosh@users.noreply.github.com> Date: Sat, 22 Jun 2024 13:15:20 +0100 Subject: [PATCH 28/36] ChannelModerate --- twitchio/ext/eventsub/models.py | 130 ++++++++++++++++++++++++++++- twitchio/ext/eventsub/server.py | 15 +++- twitchio/ext/eventsub/websocket.py | 14 ++++ 3 files changed, 155 insertions(+), 4 deletions(-) diff --git a/twitchio/ext/eventsub/models.py b/twitchio/ext/eventsub/models.py index 306a43b8..5f9c19b6 100644 --- a/twitchio/ext/eventsub/models.py +++ b/twitchio/ext/eventsub/models.py @@ -1916,6 +1916,130 @@ def __init__(self, client: EventSubClient, data: dict) -> None: self.from_automod: bool = data["from_automod"] self.terms: List[str] = data["terms"] + +class ChannelModerateData(EventData): + """ + Represents a channel moderation event. + + Attributes + ----------- + broadcaster: :class:`PartialUser` + The channel where the moderation event occurred. + moderator: :class:`PartialUser` + The moderator who performed the action. + action: :class:`str` + The action performed. + """ + + __slots__ = ( + "broadcaster", + "moderator", + "action", + "followers", + "slow", + "vip", + "unvip", + "mod", + "unmod", + "ban", + "unban", + "timeout", + "untimeout", + "raid", + "unraid", + "delete", + "automod_terms", + "unban_request", + ) + + class Followers: + def __init__(self, data: dict) -> None: + self.follow_duration_minutes: int = data["follow_duration_minutes"] + + class Slow: + def __init__(self, data: dict) -> None: + self.wait_time_seconds: int = data["wait_time_seconds"] + + class VIPStatus: + def __init__(self, client: EventSubClient, data: dict) -> None: + self.user: PartialUser = _transform_user(client, data, "user") + + class ModeratorStatus: + def __init__(self, client: EventSubClient, data: dict) -> None: + self.user: PartialUser = _transform_user(client, data, "user") + + class Ban: + def __init__(self, client: EventSubClient, data: dict) -> None: + self.user: PartialUser = _transform_user(client, data, "user") + self.reason: Optional[str] = data.get("reason") + + class UnBan: + def __init__(self, client: EventSubClient, data: dict) -> None: + self.user: PartialUser = _transform_user(client, data, "user") + + class Timeout: + def __init__(self, client: EventSubClient, data: dict) -> None: + self.user: PartialUser = _transform_user(client, data, "user") + self.reason: Optional[str] = data.get("reason") + self.expires_at: datetime.datetime = _parse_datetime(data["expires_at"]) + + class UnTimeout: + def __init__(self, client: EventSubClient, data: dict) -> None: + self.user: PartialUser = _transform_user(client, data, "user") + + class Raid: + def __init__(self, client: EventSubClient, data: dict) -> None: + self.user: PartialUser = _transform_user(client, data, "user") + self.viewer_count: int = data["viewer_count"] + + class UnRaid: + def __init__(self, client: EventSubClient, data: dict) -> None: + self.user: PartialUser = _transform_user(client, data, "user") + + class Delete: + def __init__(self, client: EventSubClient, data: dict) -> None: + self.user: PartialUser = _transform_user(client, data, "user") + self.message_id: str = data["message_id"] + self.message_body: str = data["message_body"] + + class AutoModTerms: + def __init__(self, data: dict) -> None: + self.action: str = data["action"] + self.list: str = data["list"] + self.terms: List[str] = data["terms"] + self.from_automod: bool = data["from_automod"] + + class UnBanRequest: + def __init__(self, client: EventSubClient, data: dict) -> None: + self.user: PartialUser = _transform_user(client, data, "user") + self.is_approved: bool = data["is_approved"] + self.moderator_message: str = data["moderator_message"] + + def __init__(self, client: EventSubClient, data: dict) -> None: + self.broadcaster = _transform_user(client, data, "broadcaster_user") + self.moderator = _transform_user(client, data, "moderator_user") + self.action: str = data["action"] + self.followers = self.Followers(data["followers"]) if data.get("followers") is not None else None + self.slow = self.Slow(data["slow"]) if data.get("slow") is not None else None + self.vip = self.VIPStatus(client, data["vip"]) if data.get("vip") is not None else None + self.unvip = self.VIPStatus(client, data["unvip"]) if data.get("unvip") is not None else None + self.mod = self.ModeratorStatus(client, data["mod"]) if data.get("mod") is not None else None + self.unmod = self.ModeratorStatus(client, data["unmod"]) if data.get("unmod") is not None else None + self.ban = self.Ban(client, data["ban"]) if data.get("ban") is not None else None + self.unban = self.UnBan(client, data["unban"]) if data.get("unban") is not None else None + self.timeout = self.Timeout(client, data["timeout"]) if data.get("timeout") is not None else None + self.untimeout = self.UnTimeout(client, data["untimeout"]) if data.get("untimeout") is not None else None + self.raid = self.Raid(client, data["raid"]) if data.get("raid") is not None else None + self.unraid = self.UnRaid(client, data["unraid"]) if data.get("unraid") is not None else None + self.delete = self.Delete(client, data["delete"]) if data.get("delete") is not None else None + self.automod_terms = ( + self.AutoModTerms(client, data["automod_terms"]) if data.get("automod_terms") is not None else None + ) + self.unban_request = ( + self.UnBanRequest(client, data["unban_request"]) if data.get("unban_request") is not None else None + ) + + class SuspiciousUserUpdateData(EventData): """ Represents a suspicious user update event. @@ -1941,7 +2065,6 @@ def __init__(self, client: EventSubClient, data: dict) -> None: self.trust_status: Literal["active_monitoring", "restricted", "none"] = data["low_trust_status"] - _DataType = Union[ ChannelBanData, ChannelUnbanData, @@ -1982,6 +2105,7 @@ def __init__(self, client: EventSubClient, data: dict) -> None: AutomodSettingsUpdateData, AutomodTermsUpdateData, SuspiciousUserUpdateData, + ChannelModerateData, ] @@ -2041,6 +2165,8 @@ class _SubscriptionTypes(metaclass=_SubTypesMeta): channel_charity_donate = "channel.charity_campaign.donate", 1, ChannelCharityDonationData + channel_moderate = "channel.moderate", 1, ChannelModerateData + hypetrain_begin = "channel.hype_train.begin", 1, HypeTrainBeginProgressData hypetrain_progress = "channel.hype_train.progress", 1, HypeTrainBeginProgressData hypetrain_end = "channel.hype_train.end", 1, HypeTrainEndData @@ -2064,7 +2190,7 @@ class _SubscriptionTypes(metaclass=_SubTypesMeta): user_authorization_revoke = "user.authorization.revoke", 1, UserAuthorizationRevokedData user_update = "user.update", 1, UserUpdateData - + suspicious_user_update = "channel.suspicious_user.update", 1, SuspiciousUserUpdateData diff --git a/twitchio/ext/eventsub/server.py b/twitchio/ext/eventsub/server.py index 0ceca261..838d0cfd 100644 --- a/twitchio/ext/eventsub/server.py +++ b/twitchio/ext/eventsub/server.py @@ -321,8 +321,19 @@ def subscribe_automod_terms_update( def subscribe_channel_charity_donate(self, broadcaster: Union[PartialUser, str, int]): return self._subscribe_with_broadcaster(models.SubscriptionTypes.channel_charity_donate, broadcaster) - def subscribe_suspicious_user_update(self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int]): - return self._subscribe_with_broadcaster_moderator(models.SubscriptionTypes.suspicious_user_update, broadcaster, moderator) + def subscribe_suspicious_user_update( + self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int] + ): + return self._subscribe_with_broadcaster_moderator( + models.SubscriptionTypes.suspicious_user_update, broadcaster, moderator + ) + + def subscribe_channel_moderate( + self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int] + ): + return self._subscribe_with_broadcaster_moderator( + models.SubscriptionTypes.channel_moderate, broadcaster, moderator + ) async def subscribe_user_authorization_granted(self): return await self._http.create_webhook_subscription( diff --git a/twitchio/ext/eventsub/websocket.py b/twitchio/ext/eventsub/websocket.py index 5787d420..300abca8 100644 --- a/twitchio/ext/eventsub/websocket.py +++ b/twitchio/ext/eventsub/websocket.py @@ -555,3 +555,17 @@ async def subscribe_automod_terms_update( await self._subscribe_with_broadcaster_moderator( models.SubscriptionTypes.automod_terms_update, broadcaster, moderator, token ) + + async def subscribe_suspicious_user_update( + self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int], token: str + ): + await self._subscribe_with_broadcaster_moderator( + models.SubscriptionTypes.suspicious_user_update, broadcaster, moderator, token + ) + + async def subscribe_channel_moderate( + self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int], token: str + ): + await self._subscribe_with_broadcaster_moderator( + models.SubscriptionTypes.channel_moderate, broadcaster, moderator, token + ) From 1bc381b9dd7de8297d7ed8ece9bc2d95234db872 Mon Sep 17 00:00:00 2001 From: chillymosh <86857777+chillymosh@users.noreply.github.com> Date: Sat, 22 Jun 2024 14:00:22 +0100 Subject: [PATCH 29/36] Docs --- docs/changelog.rst | 32 +++--- docs/exts/eventsub.rst | 12 +++ twitchio/ext/eventsub/models.py | 176 ++++++++++++++++++++++++++++---- 3 files changed, 184 insertions(+), 36 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5d89a7bc..e8a6149c 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -16,24 +16,28 @@ - ext.eventsub - Additions - - Added :method:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_unban_request_create ` / - :method:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_unban_request_create ` - - Added :method:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_unban_request_resolve ` / - :method:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_unban_request_resolve ` - - Added :method:`Twitchio.ext.eventsub.EventSubClient.subscribe_automod_terms_update ` / - :method:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_terms_update ` - - Added :method:`Twitchio.ext.eventsub.EventSubClient.subscribe_automod_settings_update ` / - :method:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_settings_update ` - - Added :method:`Twitchio.ext.eventsub.EventSubClient.subscribe_automod_message_update ` / - :method:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_message_update ` - - Added :method:`Twitchio.ext.eventsub.EventSubClient.subscribe_automod_message_hold ` / - :method:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_message_hold ` + - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_unban_request_create ` / + :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_unban_request_create ` + - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_unban_request_resolve ` / + :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_unban_request_resolve ` + - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_automod_terms_update ` / + :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_terms_update ` + - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_automod_settings_update ` / + :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_settings_update ` + - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_automod_message_update ` / + :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_message_update ` + - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_automod_message_hold ` / + :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_message_hold ` + - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_moderate ` / + :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_moderate ` + - Added :meth:`Twitchio.ext.eventsub.EventSubClient.channel_suspicious_user_update ` / + :meth:`Twitchio.ext.eventsub.EventSubWSClient.channel_suspicious_user_update ` - Added all accompanying models for those endpoints. - ext.sounds - Additions - Added TinyTag as a dependency to support retrieving audio metadata using TinyTag in `ext.sounds.__init__.py`. - - added :method:`Twitchio.ext.sounds.rate setter. - - added :method:`Twitchio.ext.sounds.channels setter. + - added :meth:`twitchio.ext.sounds.Sound.rate` setter. + - added :meth:`twitchio.ext.sounds.Sound.channels` setter. 2.9.2 diff --git a/docs/exts/eventsub.rst b/docs/exts/eventsub.rst index 999dcab5..786a8b74 100644 --- a/docs/exts/eventsub.rst +++ b/docs/exts/eventsub.rst @@ -530,3 +530,15 @@ API Reference .. autoclass:: AutomodTermsUpdateData :members: :inherited-members: + +.. attributetable::: SuspiciousUserUpdateData + +.. autoclass:: SuspiciousUserUpdateData + :members: + :inherited-members: + +.. attributetable::: ChannelModerateData + +.. autoclass:: ChannelModerateData + :members: + :inherited-members: diff --git a/twitchio/ext/eventsub/models.py b/twitchio/ext/eventsub/models.py index 5f9c19b6..f5961bda 100644 --- a/twitchio/ext/eventsub/models.py +++ b/twitchio/ext/eventsub/models.py @@ -1929,6 +1929,36 @@ class ChannelModerateData(EventData): The moderator who performed the action. action: :class:`str` The action performed. + followers: Optional[:class:`Followers`] + Metadata associated with the followers command. + slow: Optional[:class:`Slow`] + Metadata associated with the slow command. + vip: Optional[:class:`VIPStatus`] + Metadata associated with the vip command. + unvip: Optional[:class:`VIPStatus`] + Metadata associated with the vip command. + mod: Optional[:class:`ModStatus`] + Metadata associated with the mod command. + unmod: Optional[:class:`ModStatus`] + Metadata associated with the mod command. + ban: Optional[:class:`BanStatus`] + Metadata associated with the ban command. + unban: Optional[:class:`BanStatus`] + Metadata associated with the unban command. + timeout: Optional[:class:`TimeoutStatus`] + Metadata associated with the timeout command. + untimeout: Optional[:class:`TimeoutStatus`] + Metadata associated with the untimeout command. + raid: Optional[:class:`RaidStatus`] + Metadata associated with the raid command. + unraid: Optional[:class:`RaidStatus`] + Metadata associated with the unraid command. + delete: Optional[:class:`Delete`] + Metadata associated with the delete command. + automod_terms: Optional[:class:`AutoModTerms`] + Metadata associated with the automod terms changes. + unban_request: Optional[:class:`UnBanRequest`] + Metadata associated with an unban request. """ __slots__ = ( @@ -1953,63 +1983,165 @@ class ChannelModerateData(EventData): ) class Followers: + """ + Metadata associated with the followers command. + + Attributes: + ----------- + follow_duration_minutes: :class:`int` + The length of time, in minutes, that the followers must have followed the broadcaster to participate in the chat room. + """ + def __init__(self, data: dict) -> None: self.follow_duration_minutes: int = data["follow_duration_minutes"] class Slow: + """ + Metadata associated with the slow command. + + Attributes: + ----------- + wait_time_seconds: :class:`int` + The amount of time, in seconds, that users need to wait between sending messages. + """ + def __init__(self, data: dict) -> None: self.wait_time_seconds: int = data["wait_time_seconds"] class VIPStatus: + """ + Metadata associated with the vip / unvip command. + + Attributes: + ----------- + user: :class:`PartialUser` + The user who is gaining or losing VIP access. + """ + def __init__(self, client: EventSubClient, data: dict) -> None: self.user: PartialUser = _transform_user(client, data, "user") class ModeratorStatus: + """ + Metadata associated with the mod / unmod command. + + Attributes: + ----------- + user: :class:`PartialUser` + The user who is gaining or losing moderator access. + """ + def __init__(self, client: EventSubClient, data: dict) -> None: self.user: PartialUser = _transform_user(client, data, "user") - class Ban: + class BanStatus: + """ + Metadata associated with the ban / unban command. + + Attributes: + ----------- + user: :class:`PartialUser` + The user who is banned / unbanned. + reason: Optional[:class:`str`] + Reason for the ban. + """ + def __init__(self, client: EventSubClient, data: dict) -> None: self.user: PartialUser = _transform_user(client, data, "user") self.reason: Optional[str] = data.get("reason") - class UnBan: - def __init__(self, client: EventSubClient, data: dict) -> None: - self.user: PartialUser = _transform_user(client, data, "user") + class TimeoutStatus: + """ + Metadata associated with the timeout / untimeout command. + + Attributes: + ----------- + user: :class:`PartialUser` + The user who is timedout / untimedout. + reason: Optional[:class:`str`] + Reason for the timeout. + expires_at: Optional[:class:`datetime.datetime`] + Datetime the timeout expires. + """ - class Timeout: def __init__(self, client: EventSubClient, data: dict) -> None: self.user: PartialUser = _transform_user(client, data, "user") self.reason: Optional[str] = data.get("reason") - self.expires_at: datetime.datetime = _parse_datetime(data["expires_at"]) + self.expires_at: Optional[datetime.datetime] = ( + _parse_datetime(data["expires_at"]) if data.get("expires_at") is not None else None + ) - class UnTimeout: - def __init__(self, client: EventSubClient, data: dict) -> None: - self.user: PartialUser = _transform_user(client, data, "user") + class RaidStatus: + """ + Metadata associated with the raid / unraid command. + + Attributes: + ----------- + user: :class:`PartialUser` + The user who is timedout / untimedout. + viewer_count: :class:`int` + The viewer count. + """ - class Raid: def __init__(self, client: EventSubClient, data: dict) -> None: self.user: PartialUser = _transform_user(client, data, "user") self.viewer_count: int = data["viewer_count"] - class UnRaid: - def __init__(self, client: EventSubClient, data: dict) -> None: - self.user: PartialUser = _transform_user(client, data, "user") - class Delete: + """ + Metadata associated with the delete command. + + Attributes: + ----------- + user: :class:`PartialUser` + The user who is timedout / untimedout. + message_id: :class:`str` + The id of deleted message. + message_body: :class:`str` + The message body of the deleted message. + """ + def __init__(self, client: EventSubClient, data: dict) -> None: self.user: PartialUser = _transform_user(client, data, "user") self.message_id: str = data["message_id"] self.message_body: str = data["message_body"] class AutoModTerms: + """ + Metadata associated with the automod terms change. + + Attributes: + ----------- + action: :class:`Literal["add", "remove"]` + Either “add” or “remove”. + list: :class:`Literal["blocked", "permitted"]` + Either “blocked” or “permitted”. + terms: List[:class:`str`] + Terms being added or removed. + from_automod: :class:`bool` + Whether the terms were added due to an Automod message approve/deny action. + """ + def __init__(self, data: dict) -> None: - self.action: str = data["action"] - self.list: str = data["list"] + self.action: Literal["add", "remove"] = data["action"] + self.list: Literal["blocked", "permitted"] = data["list"] self.terms: List[str] = data["terms"] self.from_automod: bool = data["from_automod"] class UnBanRequest: + """ + Metadata associated with the slow command. + + Attributes: + ----------- + user: :class:`PartialUser` + The user who is requesting an unban. + is_approved: :class:`bool` + Whether or not the unban request was approved or denied. + moderator_message: :class:`str` + The message included by the moderator explaining their approval or denial. + """ + def __init__(self, client: EventSubClient, data: dict) -> None: self.user: PartialUser = _transform_user(client, data, "user") self.is_approved: bool = data["is_approved"] @@ -2025,12 +2157,12 @@ def __init__(self, client: EventSubClient, data: dict) -> None: self.unvip = self.VIPStatus(client, data["unvip"]) if data.get("unvip") is not None else None self.mod = self.ModeratorStatus(client, data["mod"]) if data.get("mod") is not None else None self.unmod = self.ModeratorStatus(client, data["unmod"]) if data.get("unmod") is not None else None - self.ban = self.Ban(client, data["ban"]) if data.get("ban") is not None else None - self.unban = self.UnBan(client, data["unban"]) if data.get("unban") is not None else None - self.timeout = self.Timeout(client, data["timeout"]) if data.get("timeout") is not None else None - self.untimeout = self.UnTimeout(client, data["untimeout"]) if data.get("untimeout") is not None else None - self.raid = self.Raid(client, data["raid"]) if data.get("raid") is not None else None - self.unraid = self.UnRaid(client, data["unraid"]) if data.get("unraid") is not None else None + self.ban = self.BanStatus(client, data["ban"]) if data.get("ban") is not None else None + self.unban = self.BanStatus(client, data["unban"]) if data.get("unban") is not None else None + self.timeout = self.TimeoutStatus(client, data["timeout"]) if data.get("timeout") is not None else None + self.untimeout = self.TimeoutStatus(client, data["untimeout"]) if data.get("untimeout") is not None else None + self.raid = self.RaidStatus(client, data["raid"]) if data.get("raid") is not None else None + self.unraid = self.RaidStatus(client, data["unraid"]) if data.get("unraid") is not None else None self.delete = self.Delete(client, data["delete"]) if data.get("delete") is not None else None self.automod_terms = ( self.AutoModTerms(client, data["automod_terms"]) if data.get("automod_terms") is not None else None From 3e4d4d61fb676aeab80c0f8e1749d9477ca4e9d1 Mon Sep 17 00:00:00 2001 From: chillymosh <86857777+chillymosh@users.noreply.github.com> Date: Sat, 22 Jun 2024 14:22:42 +0100 Subject: [PATCH 30/36] Update models.py --- twitchio/ext/eventsub/models.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/twitchio/ext/eventsub/models.py b/twitchio/ext/eventsub/models.py index f5961bda..208ff16e 100644 --- a/twitchio/ext/eventsub/models.py +++ b/twitchio/ext/eventsub/models.py @@ -1986,7 +1986,7 @@ class Followers: """ Metadata associated with the followers command. - Attributes: + Attributes ----------- follow_duration_minutes: :class:`int` The length of time, in minutes, that the followers must have followed the broadcaster to participate in the chat room. @@ -1999,7 +1999,7 @@ class Slow: """ Metadata associated with the slow command. - Attributes: + Attributes ----------- wait_time_seconds: :class:`int` The amount of time, in seconds, that users need to wait between sending messages. @@ -2012,7 +2012,7 @@ class VIPStatus: """ Metadata associated with the vip / unvip command. - Attributes: + Attributes ----------- user: :class:`PartialUser` The user who is gaining or losing VIP access. @@ -2025,7 +2025,7 @@ class ModeratorStatus: """ Metadata associated with the mod / unmod command. - Attributes: + Attributes ----------- user: :class:`PartialUser` The user who is gaining or losing moderator access. @@ -2038,7 +2038,7 @@ class BanStatus: """ Metadata associated with the ban / unban command. - Attributes: + Attributes ----------- user: :class:`PartialUser` The user who is banned / unbanned. @@ -2054,7 +2054,7 @@ class TimeoutStatus: """ Metadata associated with the timeout / untimeout command. - Attributes: + Attributes ----------- user: :class:`PartialUser` The user who is timedout / untimedout. @@ -2075,7 +2075,7 @@ class RaidStatus: """ Metadata associated with the raid / unraid command. - Attributes: + Attributes ----------- user: :class:`PartialUser` The user who is timedout / untimedout. @@ -2091,7 +2091,7 @@ class Delete: """ Metadata associated with the delete command. - Attributes: + Attributes ----------- user: :class:`PartialUser` The user who is timedout / untimedout. @@ -2110,7 +2110,7 @@ class AutoModTerms: """ Metadata associated with the automod terms change. - Attributes: + Attributes ----------- action: :class:`Literal["add", "remove"]` Either “add” or “remove”. @@ -2132,7 +2132,7 @@ class UnBanRequest: """ Metadata associated with the slow command. - Attributes: + Attributes ----------- user: :class:`PartialUser` The user who is requesting an unban. From e09c710fb61e241fef31e7c98cca20398de9b8bd Mon Sep 17 00:00:00 2001 From: chillymosh <86857777+chillymosh@users.noreply.github.com> Date: Sat, 22 Jun 2024 14:24:24 +0100 Subject: [PATCH 31/36] Update models.py --- twitchio/ext/eventsub/models.py | 56 ++++++++++++++++----------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/twitchio/ext/eventsub/models.py b/twitchio/ext/eventsub/models.py index 208ff16e..336d0afc 100644 --- a/twitchio/ext/eventsub/models.py +++ b/twitchio/ext/eventsub/models.py @@ -1627,9 +1627,9 @@ class ChannelUnbanRequestCreateData(EventData): ----------- id: :class:`str` The ID of the ban request. - broadcaster: :class:`PartialUser` + broadcaster: :class:`~twitchio.PartialUser` The broadcaster from which the user was banned. - user: :class:`PartialUser` + user: :class:`~twitchio.PartialUser` The user that was banned. text: :class:`str` The unban request text the user submitted. @@ -1655,11 +1655,11 @@ class ChannelUnbanRequestResolveData(EventData): ----------- id: :class:`str` The ID of the ban request. - broadcaster: :class:`PartialUser` + broadcaster: :class:`~twitchio.PartialUser` The broadcaster from which the user was banned. - user: :class:`PartialUser` + user: :class:`~twitchio.PartialUser` The user that was banned. - moderator: :class:`PartialUser` + moderator: :class:`~twitchio.PartialUser` The moderator that handled this unban request. resolution_text: :class:`str` The reasoning provided by the moderator. @@ -1687,9 +1687,9 @@ class AutomodMessageHoldData(EventData): The ID of the message. message_content: :class:`str` The contents of the message - broadcaster: :class:`PartialUser` + broadcaster: :class:`~twitchio.PartialUser` The broadcaster from which the message was held. - user: :class:`PartialUser` + user: :class:`~twitchio.PartialUser` The user that sent the message. level: :class:`int` The level of alarm raised for this message. @@ -1758,11 +1758,11 @@ class AutomodMessageUpdateData(EventData): The ID of the message. message_content: :class:`str` The contents of the message - broadcaster: :class:`PartialUser` + broadcaster: :class:`~twitchio.PartialUser` The broadcaster from which the message was held. - user: :class:`PartialUser` + user: :class:`~twitchio.PartialUser` The user that sent the message. - moderator: :class:`PartialUser` + moderator: :class:`~twitchio.PartialUser` The moderator that updated the message status. status: :class:`str` The new status of the message. Typically one of ``approved`` or ``denied``. @@ -1833,9 +1833,9 @@ class AutomodSettingsUpdateData(EventData): Attributes ------------ - broadcaster: :class:`PartialUser` + broadcaster: :class:`~twitchio.PartialUser` The broadcaster for which the settings were updated. - moderator: :class:`PartialUser` + moderator: :class:`~twitchio.PartialUser` The moderator that updated the settings. overall :class:`int` | ``None`` The overall level of automod aggressiveness. @@ -1895,9 +1895,9 @@ class AutomodTermsUpdateData(EventData): Attributes ----------- - broadcaster: :class:`PartialUser` + broadcaster: :class:`~twitchio.PartialUser` The broadcaster for which the terms were updated. - moderator: :class:`PartialUser` + moderator: :class:`~twitchio.PartialUser` The moderator who updated the terms. action: :class:`str` The action type. @@ -1923,9 +1923,9 @@ class ChannelModerateData(EventData): Attributes ----------- - broadcaster: :class:`PartialUser` + broadcaster: :class:`~twitchio.PartialUser` The channel where the moderation event occurred. - moderator: :class:`PartialUser` + moderator: :class:`~twitchio.PartialUser` The moderator who performed the action. action: :class:`str` The action performed. @@ -1937,9 +1937,9 @@ class ChannelModerateData(EventData): Metadata associated with the vip command. unvip: Optional[:class:`VIPStatus`] Metadata associated with the vip command. - mod: Optional[:class:`ModStatus`] + mod: Optional[:class:`ModeratorStatus`] Metadata associated with the mod command. - unmod: Optional[:class:`ModStatus`] + unmod: Optional[:class:`ModeratorStatus`] Metadata associated with the mod command. ban: Optional[:class:`BanStatus`] Metadata associated with the ban command. @@ -2014,7 +2014,7 @@ class VIPStatus: Attributes ----------- - user: :class:`PartialUser` + user: :class:`~twitchio.PartialUser` The user who is gaining or losing VIP access. """ @@ -2027,7 +2027,7 @@ class ModeratorStatus: Attributes ----------- - user: :class:`PartialUser` + user: :class:`~twitchio.PartialUser` The user who is gaining or losing moderator access. """ @@ -2040,7 +2040,7 @@ class BanStatus: Attributes ----------- - user: :class:`PartialUser` + user: :class:`~twitchio.PartialUser` The user who is banned / unbanned. reason: Optional[:class:`str`] Reason for the ban. @@ -2056,7 +2056,7 @@ class TimeoutStatus: Attributes ----------- - user: :class:`PartialUser` + user: :class:`~twitchio.PartialUser` The user who is timedout / untimedout. reason: Optional[:class:`str`] Reason for the timeout. @@ -2077,7 +2077,7 @@ class RaidStatus: Attributes ----------- - user: :class:`PartialUser` + user: :class:`~twitchio.PartialUser` The user who is timedout / untimedout. viewer_count: :class:`int` The viewer count. @@ -2093,7 +2093,7 @@ class Delete: Attributes ----------- - user: :class:`PartialUser` + user: :class:`~twitchio.PartialUser` The user who is timedout / untimedout. message_id: :class:`str` The id of deleted message. @@ -2134,7 +2134,7 @@ class UnBanRequest: Attributes ----------- - user: :class:`PartialUser` + user: :class:`~twitchio.PartialUser` The user who is requesting an unban. is_approved: :class:`bool` Whether or not the unban request was approved or denied. @@ -2178,11 +2178,11 @@ class SuspiciousUserUpdateData(EventData): Attributes ----------- - broadcaster: :class:`PartialUser` + broadcaster: :class:`~twitchio.PartialUser` The channel where the treatment for a suspicious user was updated. - moderator: :class:`PartialUser` + moderator: :class:`~twitchio.PartialUser` The moderator who updated the terms. - user: :class:`PartialUser` + user: :class:`~twitchio.PartialUser` The the user that sent the message. trust_status: :class:`Literal["active_monitoring", "restricted", "none"]` The status set for the suspicious user. Can be the following: “none”, “active_monitoring”, or “restricted”. From d18c8ec24585292fd6abe960206bfdd73f92bab0 Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Sat, 22 Jun 2024 15:51:36 -0700 Subject: [PATCH 32/36] eventsub additions --- docs/changelog.rst | 8 ++- twitchio/ext/eventsub/models.py | 97 +++++++++++++++++++++++++++--- twitchio/ext/eventsub/server.py | 9 +++ twitchio/ext/eventsub/websocket.py | 13 +++- 4 files changed, 117 insertions(+), 10 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index e8a6149c..79f162c3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -30,8 +30,12 @@ :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_message_hold ` - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_moderate ` / :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_moderate ` - - Added :meth:`Twitchio.ext.eventsub.EventSubClient.channel_suspicious_user_update ` / - :meth:`Twitchio.ext.eventsub.EventSubWSClient.channel_suspicious_user_update ` + - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_suspicious_user_update ` / + :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_suspicious_user_update ` + - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_vip_add ` / + :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_vip_add ` + - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_vip_remove ` / + :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_vip_remove ` - Added all accompanying models for those endpoints. - ext.sounds - Additions diff --git a/twitchio/ext/eventsub/models.py b/twitchio/ext/eventsub/models.py index 336d0afc..4fe31bba 100644 --- a/twitchio/ext/eventsub/models.py +++ b/twitchio/ext/eventsub/models.py @@ -145,12 +145,10 @@ class BaseEvent: __slots__ = ("_client", "_raw_data", "subscription", "headers") @overload - def __init__(self, client: EventSubClient, _data: str, request: web.Request): - ... + def __init__(self, client: EventSubClient, _data: str, request: web.Request): ... @overload - def __init__(self, client: EventSubWSClient, _data: dict, request: None): - ... + def __init__(self, client: EventSubWSClient, _data: dict, request: None): ... def __init__( self, client: Union[EventSubClient, EventSubWSClient], _data: Union[str, dict], request: Optional[web.Request] @@ -2164,9 +2162,7 @@ def __init__(self, client: EventSubClient, data: dict) -> None: self.raid = self.RaidStatus(client, data["raid"]) if data.get("raid") is not None else None self.unraid = self.RaidStatus(client, data["unraid"]) if data.get("unraid") is not None else None self.delete = self.Delete(client, data["delete"]) if data.get("delete") is not None else None - self.automod_terms = ( - self.AutoModTerms(client, data["automod_terms"]) if data.get("automod_terms") is not None else None - ) + self.automod_terms = self.AutoModTerms(data["automod_terms"]) if data.get("automod_terms") is not None else None self.unban_request = ( self.UnBanRequest(client, data["unban_request"]) if data.get("unban_request") is not None else None ) @@ -2197,6 +2193,86 @@ def __init__(self, client: EventSubClient, data: dict) -> None: self.trust_status: Literal["active_monitoring", "restricted", "none"] = data["low_trust_status"] +class AutoCustomReward: + """ + A reward object for an Auto Reward Redeem. + + Attributes + ----------- + type: :class:`str` + The type of the reward. One of ``single_message_bypass_sub_mode``, ``send_highlighted_message``, ``random_sub_emote_unlock``, + ``chosen_sub_emote_unlock``, ``chosen_modified_sub_emote_unlock``, ``message_effect``, ``gigantify_an_emote``, ``celebration``. + cost: :class:`int` + How much the reward costs. + unlocked_emote_id: Optional[:class:`str`] + The unlocked emote, if applicable. + unlocked_emote_name: Optional[:class:`str`] + The unlocked emote, if applicable. + """ + + def __init__(self, data: dict): + self.type: str = data["type"] + self.cost: int = data["cost"] + self.unlocked_emote_id: Optional[str] = data["unlocked_emote"] and data["unlocked_emote"]["id"] + self.unlocked_emote_name: Optional[str] = data["unlocked_emote"] and data["unlocked_emote"]["name"] + + +class AutoRewardRedeem(EventData): + """ + Represents an automatic reward redemption. + + Attributes + ----------- + broadcaster: :class:`~twitchio.PartialUser` + The channel where the reward was redeemed. + user: :class:`~twitchio.PartialUser` + The user that redeemed the reward. + id: :class:`str` + The ID of the redemption. + reward: :class:`AutoCustomReward` + The reward that was redeemed. + message: :class:`str` + The message the user sent. + message_emotes: :class:`dict` + The emote data for the message. + user_input: Optional[:class:`str`] + The input to the reward, if it requires any. + redeemed_at: :class:`datetime.datetime` + When the reward was redeemed. + """ + + __slots__ = ("broadcaster", "user", "id", "reward", "message", "message_emotes", "user_input", "redeemed_at") + + def __init__(self, client: EventSubClient, data: dict) -> None: + self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster") + self.user: PartialUser = _transform_user(client, data, "user") + self.id: str = data["id"] + self.reward = AutoCustomReward(data["reward"]) + self.message: str = data["message"] + self.message_emotes: dict = data["message_emotes"] + self.user_input: Optional[str] = data["user_input"] + self.redeemed_at: datetime.datetime = _parse_datetime(data["redeemed_at"]) + + +class ChannelVIPAddRemove(EventData): + """ + Represents a VIP being added/removed from a channel. + + Attributes + ----------- + broadcaster: :class:`~twitchio.PartialUser` + The channel that the VIP was added/removed from. + user: :class:`~twitchio.PartialUser` + The user that was added/removed as a VIP. + """ + + __slots__ = ("broadcaster", "user") + + def __init__(self, client: EventSubClient, data: dict) -> None: + self.broadcaster: PartialUser = _transform_user(client, data, "broadcaster") + self.user: PartialUser = _transform_user(client, data, "user") + + _DataType = Union[ ChannelBanData, ChannelUnbanData, @@ -2238,6 +2314,8 @@ def __init__(self, client: EventSubClient, data: dict) -> None: AutomodTermsUpdateData, SuspiciousUserUpdateData, ChannelModerateData, + AutoRewardRedeem, + ChannelVIPAddRemove, ] @@ -2285,6 +2363,8 @@ class _SubscriptionTypes(metaclass=_SubTypesMeta): CustomRewardRedemptionAddUpdateData, ) + auto_reward_redeem = "channel.channel_points_automatic_reward_redemption.add", 1, AutoRewardRedeem + channel_goal_begin = "channel.goal.begin", 1, ChannelGoalBeginProgressData channel_goal_progress = "channel.goal.progress", 1, ChannelGoalBeginProgressData channel_goal_end = "channel.goal.end", 1, ChannelGoalEndData @@ -2318,6 +2398,9 @@ class _SubscriptionTypes(metaclass=_SubTypesMeta): unban_request_create = "channel.unban_request.create", 1, ChannelUnbanRequestCreateData unban_request_resolve = "channel.unban_request.resolve", 1, ChannelUnbanRequestResolveData + channel_vip_add = "channel.vip.add", 1, ChannelVIPAddRemove + channel_vip_remove = "channel.vip.remove", 1, ChannelVIPAddRemove + user_authorization_grant = "user.authorization.grant", 1, UserAuthorizationGrantedData user_authorization_revoke = "user.authorization.revoke", 1, UserAuthorizationRevokedData diff --git a/twitchio/ext/eventsub/server.py b/twitchio/ext/eventsub/server.py index 838d0cfd..b9fc58ea 100644 --- a/twitchio/ext/eventsub/server.py +++ b/twitchio/ext/eventsub/server.py @@ -248,6 +248,9 @@ def subscribe_channel_prediction_lock(self, broadcaster: Union[PartialUser, str, def subscribe_channel_prediction_end(self, broadcaster: Union[PartialUser, str, int]): return self._subscribe_with_broadcaster(models.SubscriptionTypes.prediction_end, broadcaster) + def subscribe_channel_auto_reward_redeem(self, broadcaster: Union[PartialUser, str, int]): + return self._subscribe_with_broadcaster(models.SubscriptionTypes.auto_reward_redeem, broadcaster) + def subscribe_channel_shield_mode_begin( self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int] ): @@ -335,6 +338,12 @@ def subscribe_channel_moderate( models.SubscriptionTypes.channel_moderate, broadcaster, moderator ) + def subscribe_channel_vip_add(self, broadcaster: Union[PartialUser, str, int]): + return self._subscribe_with_broadcaster(models.SubscriptionTypes.channel_vip_add, broadcaster) + + def subscribe_channel_vip_remove(self, broadcaster: Union[PartialUser, str, int]): + return self._subscribe_with_broadcaster(models.SubscriptionTypes.channel_vip_remove, broadcaster) + async def subscribe_user_authorization_granted(self): return await self._http.create_webhook_subscription( models.SubscriptionTypes.user_authorization_grant, {"client_id": self.client._http.client_id} diff --git a/twitchio/ext/eventsub/websocket.py b/twitchio/ext/eventsub/websocket.py index 300abca8..ae759332 100644 --- a/twitchio/ext/eventsub/websocket.py +++ b/twitchio/ext/eventsub/websocket.py @@ -97,7 +97,9 @@ def __init__(self, client: Client, http: http.EventSubHTTP): self._pump_task: Optional[asyncio.Task] = None self._timeout: Optional[int] = None self._session_id: Optional[str] = None - self._target_user_id: int | None = None # each websocket can only have one authenticated user on it for some bizzare reason, but this isnt documented anywhere + self._target_user_id: int | None = ( + None # each websocket can only have one authenticated user on it for some bizzare reason, but this isnt documented anywhere + ) self.remaining_slots: int = 300 # default to 300 def __hash__(self) -> int: @@ -483,6 +485,9 @@ async def subscribe_channel_prediction_lock(self, broadcaster: Union[PartialUser async def subscribe_channel_prediction_end(self, broadcaster: Union[PartialUser, str, int], token: str): await self._subscribe_with_broadcaster(models.SubscriptionTypes.prediction_end, broadcaster, token) + async def subscribe_channel_auto_reward_redeem(self, broadcaster: Union[PartialUser, str, int], token: str): + await self._subscribe_with_broadcaster(models.SubscriptionTypes.auto_reward_redeem, broadcaster, token) + async def subscribe_channel_shield_mode_begin( self, broadcaster: Union[PartialUser, str, int], moderator: Union[PartialUser, str, int], token: str ): @@ -569,3 +574,9 @@ async def subscribe_channel_moderate( await self._subscribe_with_broadcaster_moderator( models.SubscriptionTypes.channel_moderate, broadcaster, moderator, token ) + + async def subscribe_channel_vip_add(self, broadcaster: Union[PartialUser, str, int], token: str): + await self._subscribe_with_broadcaster(models.SubscriptionTypes.channel_vip_add, broadcaster, token) + + async def subscribe_channel_vip_remove(self, broadcaster: Union[PartialUser, str, int], token: str): + await self._subscribe_with_broadcaster(models.SubscriptionTypes.channel_vip_remove, broadcaster, token) From 05d7eb326b46c72940f54c6f17a2ab978006b7c3 Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Sat, 22 Jun 2024 15:59:27 -0700 Subject: [PATCH 33/36] version bump --- twitchio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twitchio/__init__.py b/twitchio/__init__.py index 0d024958..6b355fe7 100644 --- a/twitchio/__init__.py +++ b/twitchio/__init__.py @@ -28,7 +28,7 @@ __author__ = "TwitchIO, PythonistaGuild" __license__ = "MIT" __copyright__ = "Copyright 2017-present (c) TwitchIO" -__version__ = "2.9.2" +__version__ = "2.10.0" from .client import Client from .user import * From 6390c3dc722cd12d7d756ca78c26acba4e58835f Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Sat, 22 Jun 2024 16:02:22 -0700 Subject: [PATCH 34/36] eventsub doc --- docs/exts/eventsub.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/exts/eventsub.rst b/docs/exts/eventsub.rst index 786a8b74..ea0a4b91 100644 --- a/docs/exts/eventsub.rst +++ b/docs/exts/eventsub.rst @@ -542,3 +542,15 @@ API Reference .. autoclass:: ChannelModerateData :members: :inherited-members: + +.. autoclass:: ChannelVIPAddRemove + :members: + :inherited-members: + +.. autoclass:: AutoCustomReward + :members: + :inherited-members: + +.. autoclass:: AutoRewardRedeem + :members: + :inherited-members: From 79b81ad4549eb6d1705ece3b2482a1e40da08193 Mon Sep 17 00:00:00 2001 From: IAmTomahawkx Date: Sat, 22 Jun 2024 16:16:28 -0700 Subject: [PATCH 35/36] fix doc linking in changelog --- docs/changelog.rst | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 79f162c3..5c39e093 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -9,37 +9,37 @@ - ext.commands - Changes - - Added which alias failed to load in the error raised by :func:`~twitchio.ext.commands.Context.add_command` + - Added which alias failed to load in the error raised by :func:`~twitchio.ext.commands.Bot.add_command` - Bug fixes - fix string parser not properly parsing specific quoted strings - ext.eventsub - Additions - - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_unban_request_create ` / - :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_unban_request_create ` - - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_unban_request_resolve ` / - :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_unban_request_resolve ` - - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_automod_terms_update ` / - :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_terms_update ` - - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_automod_settings_update ` / - :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_settings_update ` - - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_automod_message_update ` / - :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_message_update ` - - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_automod_message_hold ` / - :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_automod_message_hold ` - - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_moderate ` / - :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_moderate ` - - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_suspicious_user_update ` / - :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_suspicious_user_update ` - - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_vip_add ` / - :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_vip_add ` - - Added :meth:`Twitchio.ext.eventsub.EventSubClient.subscribe_channel_vip_remove ` / - :meth:`Twitchio.ext.eventsub.EventSubWSClient.subscribe_channel_vip_remove ` + - Added :meth:`EventSubClient.subscribe_channel_unban_request_create ` / + :meth:`EventSubWSClient.subscribe_channel_unban_request_create ` + - Added :meth:`EventSubClient.subscribe_channel_unban_request_resolve ` / + :meth:`EventSubWSClient.subscribe_channel_unban_request_resolve ` + - Added :meth:`EventSubClient.subscribe_automod_terms_update ` / + :meth:`EventSubWSClient.subscribe_automod_terms_update ` + - Added :meth:`EventSubClient.subscribe_automod_settings_update ` / + :meth:`EventSubWSClient.subscribe_automod_settings_update ` + - Added :meth:`EventSubClient.subscribe_automod_message_update ` / + :meth:`EventSubWSClient.subscribe_automod_message_update ` + - Added :meth:`EventSubClient.subscribe_automod_message_hold ` / + :meth:`EventSubWSClient.subscribe_automod_message_hold ` + - Added :meth:`EventSubClient.subscribe_channel_moderate ` / + :meth:`EventSubWSClient.subscribe_channel_moderate ` + - Added :meth:`EventSubClient.subscribe_suspicious_user_update ` / + :meth:`EventSubWSClient.subscribe_suspicious_user_update ` + - Added :meth:`EventSubClient.subscribe_channel_vip_add ` / + :meth:`EventSubWSClient.subscribe_channel_vip_add ` + - Added :meth:`EventSubClient.subscribe_channel_vip_remove ` / + :meth:`EventSubWSClient.subscribe_channel_vip_remove ` - Added all accompanying models for those endpoints. - ext.sounds - Additions - - Added TinyTag as a dependency to support retrieving audio metadata using TinyTag in `ext.sounds.__init__.py`. + - Added TinyTag as a dependency to support retrieving audio metadata. - added :meth:`twitchio.ext.sounds.Sound.rate` setter. - added :meth:`twitchio.ext.sounds.Sound.channels` setter. From b91434bf0c7cbcefa7edad13a1635f34c3da794e Mon Sep 17 00:00:00 2001 From: chillymosh <86857777+chillymosh@users.noreply.github.com> Date: Sun, 23 Jun 2024 17:25:49 +0100 Subject: [PATCH 36/36] Fix linting --- twitchio/ext/eventsub/models.py | 6 ++++-- twitchio/ext/eventsub/websocket.py | 4 +--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/twitchio/ext/eventsub/models.py b/twitchio/ext/eventsub/models.py index 4fe31bba..108a1b15 100644 --- a/twitchio/ext/eventsub/models.py +++ b/twitchio/ext/eventsub/models.py @@ -145,10 +145,12 @@ class BaseEvent: __slots__ = ("_client", "_raw_data", "subscription", "headers") @overload - def __init__(self, client: EventSubClient, _data: str, request: web.Request): ... + def __init__(self, client: EventSubClient, _data: str, request: web.Request): + ... @overload - def __init__(self, client: EventSubWSClient, _data: dict, request: None): ... + def __init__(self, client: EventSubWSClient, _data: dict, request: None): + ... def __init__( self, client: Union[EventSubClient, EventSubWSClient], _data: Union[str, dict], request: Optional[web.Request] diff --git a/twitchio/ext/eventsub/websocket.py b/twitchio/ext/eventsub/websocket.py index ae759332..352b0d77 100644 --- a/twitchio/ext/eventsub/websocket.py +++ b/twitchio/ext/eventsub/websocket.py @@ -97,9 +97,7 @@ def __init__(self, client: Client, http: http.EventSubHTTP): self._pump_task: Optional[asyncio.Task] = None self._timeout: Optional[int] = None self._session_id: Optional[str] = None - self._target_user_id: int | None = ( - None # each websocket can only have one authenticated user on it for some bizzare reason, but this isnt documented anywhere - ) + self._target_user_id: int | None = None # each websocket can only have one authenticated user on it for some bizzare reason, but this isnt documented anywhere self.remaining_slots: int = 300 # default to 300 def __hash__(self) -> int: