From 947f19afbce1a0b32491a2a515c7e8d549724ff9 Mon Sep 17 00:00:00 2001 From: Miwory Date: Thu, 11 Dec 2025 16:13:06 +0300 Subject: [PATCH 01/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=8D=D0=BD=D0=B4=D0=BF=D0=BE=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=20get=5Fchannel=5Femotes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/twitchclient/api.py | 17 +++++++++++++++++ src/twitchclient/schema.py | 28 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/twitchclient/api.py b/src/twitchclient/api.py index f5f7260..f44e006 100644 --- a/src/twitchclient/api.py +++ b/src/twitchclient/api.py @@ -745,3 +745,20 @@ class TwitchAPIClient(AioHTTPXClient): case _: raise s.Error(req.status_code, 'Internal Server Error') + + async def get_channel_emotes(self, access_token: str, broadcaster_id: int): + req = await self.get( + '/emotes', + headers={'Authorization': f'Bearer {access_token}'}, + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.ChannelEmotes.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.Error(req.status_code, req.json()['message']) + + case _: + raise s.Error(req.status_code, 'Internal Server Error') diff --git a/src/twitchclient/schema.py b/src/twitchclient/schema.py index e7d4374..1eacdd0 100644 --- a/src/twitchclient/schema.py +++ b/src/twitchclient/schema.py @@ -475,3 +475,31 @@ class Chatters(BaseModel): data: list[ChattersData] pagination: Pagination | dict[Any, Any] | None = None total: int + + +class ChannelEmoteImages(BaseModel): + model_config = ConfigDict(extra='forbid') + + url_1x: str + url_2x: str + url_4x: str + + +class ChannelEmote(BaseModel): + model_config = ConfigDict(extra='forbid') + + id: int + name: str + images: ChannelEmoteImages + tier: int + emote_type: Literal['bitstier', 'follower', 'subscriptions'] + format: list[Literal['animated', 'static']] + scale: list[Literal['1.0', '2.0', '3.0']] + theme_mode: list[Literal['dark', 'light']] + + +class ChannelEmotes(BaseModel): + model_config = ConfigDict(extra='forbid') + + data: list[ChannelEmote] + template: str -- 2.47.2 From 2c5c650ae72f61a9b16bb46ce0f3737cd8b4dc7b Mon Sep 17 00:00:00 2001 From: Miwory Date: Thu, 11 Dec 2025 16:14:43 +0300 Subject: [PATCH 02/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20cache=5Ftime=20=D0=B4=D0=BB=D1=8F=20get=5Fchanne?= =?UTF-8?q?l=5Femotes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/twitchclient/api.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/twitchclient/api.py b/src/twitchclient/api.py index f44e006..d0ce209 100644 --- a/src/twitchclient/api.py +++ b/src/twitchclient/api.py @@ -746,10 +746,20 @@ class TwitchAPIClient(AioHTTPXClient): case _: raise s.Error(req.status_code, 'Internal Server Error') - async def get_channel_emotes(self, access_token: str, broadcaster_id: int): + async def get_channel_emotes( + self, + access_token: str, + broadcaster_id: int, + cache_time: int | None = None, + ): req = await self.get( '/emotes', - headers={'Authorization': f'Bearer {access_token}'}, + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), params={'broadcaster_id': broadcaster_id}, ) -- 2.47.2 From 1a0999f3329c940f252b59fe3fb437de358f0874 Mon Sep 17 00:00:00 2001 From: Miwory Date: Thu, 11 Dec 2025 16:15:03 +0300 Subject: [PATCH 03/15] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3543bd4..48c1d6d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "twitchclient" -version = "0.1.0" +version = "0.2.0" description = "Client for Twitch API" readme = "README.md" authors = [ -- 2.47.2 From b0d3ca9addca428298826c80c399c1e3a88facac Mon Sep 17 00:00:00 2001 From: Miwory Date: Thu, 11 Dec 2025 16:16:30 +0300 Subject: [PATCH 04/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20get=5Fglobal=5Femotes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/twitchclient/api.py | 25 ++++++++++++++++++++++++- src/twitchclient/schema.py | 4 ++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/twitchclient/api.py b/src/twitchclient/api.py index d0ce209..bdf8db3 100644 --- a/src/twitchclient/api.py +++ b/src/twitchclient/api.py @@ -753,7 +753,7 @@ class TwitchAPIClient(AioHTTPXClient): cache_time: int | None = None, ): req = await self.get( - '/emotes', + '/chat/emotes', headers=self.clean_dict( { 'Authorization': f'Bearer {access_token}', @@ -772,3 +772,26 @@ class TwitchAPIClient(AioHTTPXClient): case _: raise s.Error(req.status_code, 'Internal Server Error') + + async def get_global_emotes( + self, access_token: str, cache_time: int | None = None + ): + req = await self.get( + '/chat/emotes/global', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + ) + + match req.status_code: + case st.OK: + return s.GlobalEmotes.model_validate(req.json()) + + case st.UNAUTHORIZED: + raise s.Error(req.status_code, req.json()['message']) + + case _: + raise s.Error(req.status_code, 'Internal Server Error') diff --git a/src/twitchclient/schema.py b/src/twitchclient/schema.py index 1eacdd0..fd9da2c 100644 --- a/src/twitchclient/schema.py +++ b/src/twitchclient/schema.py @@ -503,3 +503,7 @@ class ChannelEmotes(BaseModel): data: list[ChannelEmote] template: str + + +class GlobalEmotes(ChannelEmotes): + pass -- 2.47.2 From 33d498015bbc550fff60418513ef2243cb212255 Mon Sep 17 00:00:00 2001 From: Miwory Date: Thu, 11 Dec 2025 16:18:28 +0300 Subject: [PATCH 05/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20get=5Femote=5Fsets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/twitchclient/api.py | 27 +++++++++++++++++++++++++++ src/twitchclient/schema.py | 4 ++++ 2 files changed, 31 insertions(+) diff --git a/src/twitchclient/api.py b/src/twitchclient/api.py index bdf8db3..ccfd329 100644 --- a/src/twitchclient/api.py +++ b/src/twitchclient/api.py @@ -795,3 +795,30 @@ class TwitchAPIClient(AioHTTPXClient): case _: raise s.Error(req.status_code, 'Internal Server Error') + + async def get_emote_sets( + self, + access_token: str, + emote_set_id: int, + cache_time: int | None = None, + ): + req = await self.get( + '/chat/emotes/set', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params={'emote_set_id': emote_set_id}, + ) + + match req.status_code: + case st.OK: + return s.EmoteSets.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.Error(req.status_code, req.json()['message']) + + case _: + raise s.Error(req.status_code, 'Internal Server Error') diff --git a/src/twitchclient/schema.py b/src/twitchclient/schema.py index fd9da2c..6c84e4c 100644 --- a/src/twitchclient/schema.py +++ b/src/twitchclient/schema.py @@ -507,3 +507,7 @@ class ChannelEmotes(BaseModel): class GlobalEmotes(ChannelEmotes): pass + + +class EmoteSets(ChannelEmotes): + pass -- 2.47.2 From 3c6154913a7f30dfb52b99a3db5a160122ef4b7a Mon Sep 17 00:00:00 2001 From: Miwory Date: Thu, 11 Dec 2025 16:21:55 +0300 Subject: [PATCH 06/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20get=5Fchannel=5Fchat=5Fbadges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/twitchclient/api.py | 27 +++++++++++++++++++++++++++ src/twitchclient/schema.py | 26 ++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/twitchclient/api.py b/src/twitchclient/api.py index ccfd329..cd0e82b 100644 --- a/src/twitchclient/api.py +++ b/src/twitchclient/api.py @@ -822,3 +822,30 @@ class TwitchAPIClient(AioHTTPXClient): case _: raise s.Error(req.status_code, 'Internal Server Error') + + async def get_channel_chat_badges( + self, + access_token: str, + broadcaster_id: int | str, + cache_time: int | None = None, + ): + req = await self.get( + '/chat/badges', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.ChannelChatBadges.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.Error(req.status_code, req.json()['message']) + + case _: + raise s.Error(req.status_code, 'Internal Server Error') diff --git a/src/twitchclient/schema.py b/src/twitchclient/schema.py index 6c84e4c..5c20c5c 100644 --- a/src/twitchclient/schema.py +++ b/src/twitchclient/schema.py @@ -511,3 +511,29 @@ class GlobalEmotes(ChannelEmotes): class EmoteSets(ChannelEmotes): pass + + +class ChannelChatBadgesVersion(BaseModel): + model_config = ConfigDict(extra='forbid') + + id: str + image_url_1x: str + image_url_2x: str + image_url_4x: str + title: str + description: str + click_action: str | None + click_url: str | None + + +class ChannelChatBadgesData(BaseModel): + model_config = ConfigDict(extra='forbid') + + set_id: str + versions: list[ChannelChatBadgesVersion] + + +class ChannelChatBadges(BaseModel): + model_config = ConfigDict(extra='forbid') + + data: list[ChannelChatBadgesData] -- 2.47.2 From 035a87e0c5d59f4905e62b6bc505959817745cbf Mon Sep 17 00:00:00 2001 From: Miwory Date: Thu, 11 Dec 2025 16:24:00 +0300 Subject: [PATCH 07/15] =?UTF-8?q?=D0=A3=D0=BF=D1=80=D0=BE=D1=89=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=82=D0=B8=D0=BF=D0=B8=D0=B7=D0=B0=D1=86=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B4=D0=BB=D1=8F=20broadcaster=5Fid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/twitchclient/api.py | 48 ++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/twitchclient/api.py b/src/twitchclient/api.py index cd0e82b..00cdfb1 100644 --- a/src/twitchclient/api.py +++ b/src/twitchclient/api.py @@ -29,7 +29,9 @@ class TwitchAPIClient(AioHTTPXClient): logger='Twitch API', ) - async def start_commercial(self, access_token: str, broadcaster_id: int): + async def start_commercial( + self, access_token: str, broadcaster_id: int | str + ): req = await self.get( '/channels/commercial', headers={'Authorization': f'Bearer {access_token}'}, @@ -53,7 +55,9 @@ class TwitchAPIClient(AioHTTPXClient): case _: raise s.Error(req.status_code, 'Internal Server Error') - async def get_ad_schedule(self, access_token: str, broadcaster_id: int): + async def get_ad_schedule( + self, access_token: str, broadcaster_id: int | str + ): req = await self.get( '/channels/ads', headers={'Authorization': f'Bearer {access_token}'}, @@ -72,7 +76,9 @@ class TwitchAPIClient(AioHTTPXClient): case _: raise s.Error(req.status_code, 'Internal Server Error') - async def snooze_next_ad(self, access_token: str, broadcaster_id: int): + async def snooze_next_ad( + self, access_token: str, broadcaster_id: int | str + ): req = await self.post( '/channels/ads/schedule/snooze', headers={'Authorization': f'Bearer {access_token}'}, @@ -193,7 +199,7 @@ class TwitchAPIClient(AioHTTPXClient): raise s.Error(req.status_code, 'Internal Server Error') async def get_cheermotes( - self, access_token: str, broadcaster_id: int | None = None + self, access_token: str, broadcaster_id: int | str | None = None ): req = await self.get( '/bits/cheermotes', @@ -243,7 +249,9 @@ class TwitchAPIClient(AioHTTPXClient): raise s.Error(req.status_code, 'Internal Server Error') async def get_channel_information( - self, access_token: str, broadcaster_id: int | list[int] + self, + access_token: str, + broadcaster_id: int | list[int] | str | list[str], ): req = await self.get( '/channels', @@ -264,9 +272,9 @@ class TwitchAPIClient(AioHTTPXClient): async def modify_channel_information( self, access_token: str, - broadcaster_id: int, + broadcaster_id: int | str, *, - game_id: int | None = None, + game_id: int | str | None = None, broadcaster_language: str | None = None, title: str | None = None, delay: int | None = None, @@ -309,7 +317,7 @@ class TwitchAPIClient(AioHTTPXClient): raise s.Error(req.status_code, 'Internal Server Error') async def get_channel_editors( - self, access_token: str, broadcaster_id: int + self, access_token: str, broadcaster_id: int | str ): req = await self.get( '/channels/editors', @@ -330,7 +338,7 @@ class TwitchAPIClient(AioHTTPXClient): async def get_followed_channels( self, access_token: str, - broadcaster_id: int, + broadcaster_id: int | str, *, first: int = 20, after: str | None = None, @@ -360,7 +368,7 @@ class TwitchAPIClient(AioHTTPXClient): async def get_channel_followers( self, access_token: str, - broadcaster_id: int, + broadcaster_id: int | str, *, user_id: int | None = None, first: int = 20, @@ -392,7 +400,7 @@ class TwitchAPIClient(AioHTTPXClient): async def create_custom_rewards( self, access_token: str, - broadcaster_id: int, + broadcaster_id: int | str, title: str, cost: int, *, @@ -451,7 +459,7 @@ class TwitchAPIClient(AioHTTPXClient): raise s.Error(req.status_code, 'Internal Server Error') async def delete_custom_reward( - self, access_token: str, broadcaster_id: str, reward_id: str + self, access_token: str, broadcaster_id: int | str, reward_id: str ): req = await self.delete( '/channel_points/custom_rewards', @@ -478,7 +486,7 @@ class TwitchAPIClient(AioHTTPXClient): async def get_custom_rewards( self, access_token: str, - broadcaster_id: int, + broadcaster_id: int | str, *, reward_id: str | None = None, only_manageable_rewards: bool = False, @@ -514,7 +522,7 @@ class TwitchAPIClient(AioHTTPXClient): async def get_custom_reward_redemption( self, access_token: str, - broadcaster_id: int, + broadcaster_id: int | str, reward_id: str, status: Literal['CANCELED', 'FULFILLED', 'UNFULFILLED'], *, @@ -559,7 +567,7 @@ class TwitchAPIClient(AioHTTPXClient): async def update_custom_reward( self, access_token: str, - broadcaster_id: int, + broadcaster_id: int | str, reward_id: str, *, title: str | None = None, @@ -623,7 +631,7 @@ class TwitchAPIClient(AioHTTPXClient): async def update_redemption_status( self, access_token: str, - broadcaster_id: int, + broadcaster_id: int | str, reward_id: int, redemption_id: int | list[int], status: Literal['CANCELED', 'FULFILLED'], @@ -656,7 +664,7 @@ class TwitchAPIClient(AioHTTPXClient): raise s.Error(req.status_code, 'Internal Server Error') async def get_charity_campaign( - self, access_token: str, broadcaster_id: int + self, access_token: str, broadcaster_id: int | str ): req = await self.get( '/charity/campaigns', @@ -677,7 +685,7 @@ class TwitchAPIClient(AioHTTPXClient): async def get_charity_campaign_donations( self, access_token: str, - broadcaster_id: int, + broadcaster_id: int | str, first: int = 20, after: str | None = None, cache_time: int | None = None, @@ -712,7 +720,7 @@ class TwitchAPIClient(AioHTTPXClient): async def get_chatters( self, access_token: str, - broadcaster_id: int, + broadcaster_id: int | str, moderator_id: int, first: int = 20, after: str | None = None, @@ -749,7 +757,7 @@ class TwitchAPIClient(AioHTTPXClient): async def get_channel_emotes( self, access_token: str, - broadcaster_id: int, + broadcaster_id: int | str, cache_time: int | None = None, ): req = await self.get( -- 2.47.2 From 0d9ac981b86d2db7d9962bd617982f49bf3963e0 Mon Sep 17 00:00:00 2001 From: Miwory Date: Thu, 11 Dec 2025 16:24:43 +0300 Subject: [PATCH 08/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20get=5Fglobal=5Fchat=5Fbadges?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/twitchclient/api.py | 23 +++++++++++++++++++++++ src/twitchclient/schema.py | 4 ++++ 2 files changed, 27 insertions(+) diff --git a/src/twitchclient/api.py b/src/twitchclient/api.py index 00cdfb1..7447e18 100644 --- a/src/twitchclient/api.py +++ b/src/twitchclient/api.py @@ -857,3 +857,26 @@ class TwitchAPIClient(AioHTTPXClient): case _: raise s.Error(req.status_code, 'Internal Server Error') + + async def get_global_chat_badges( + self, access_token: str, cache_time: int | None = None + ): + req = await self.get( + '/chat/badges/global', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + ) + + match req.status_code: + case st.OK: + return s.GlobalChatBadges.model_validate(req.json()) + + case st.UNAUTHORIZED: + raise s.Error(req.status_code, req.json()['message']) + + case _: + raise s.Error(req.status_code, 'Internal Server Error') diff --git a/src/twitchclient/schema.py b/src/twitchclient/schema.py index 5c20c5c..a4bf559 100644 --- a/src/twitchclient/schema.py +++ b/src/twitchclient/schema.py @@ -537,3 +537,7 @@ class ChannelChatBadges(BaseModel): model_config = ConfigDict(extra='forbid') data: list[ChannelChatBadgesData] + + +class GlobalChatBadges(ChannelChatBadges): + pass -- 2.47.2 From f04916b7fd3bff5796e84d1bcf1a700047f2736a Mon Sep 17 00:00:00 2001 From: Miwory Date: Thu, 11 Dec 2025 16:34:35 +0300 Subject: [PATCH 09/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20get=5Fchat=5Fsettings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/twitchclient/api.py | 33 +++++++++++++++++++++++++++++++++ src/twitchclient/schema.py | 22 ++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/twitchclient/api.py b/src/twitchclient/api.py index 7447e18..9852396 100644 --- a/src/twitchclient/api.py +++ b/src/twitchclient/api.py @@ -880,3 +880,36 @@ class TwitchAPIClient(AioHTTPXClient): case _: raise s.Error(req.status_code, 'Internal Server Error') + + async def get_chat_settings( + self, + access_token: str, + broadcaster_id: int | str, + moderator_id: int | str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/chat/settings', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + } + ), + ) + + match req.status_code: + case st.OK: + return s.ChatSettings.model_validate(req.json()).data + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.Error(req.status_code, req.json()['message']) + + case _: + raise s.Error(req.status_code, 'Internal Server Error') diff --git a/src/twitchclient/schema.py b/src/twitchclient/schema.py index a4bf559..a72aa20 100644 --- a/src/twitchclient/schema.py +++ b/src/twitchclient/schema.py @@ -541,3 +541,25 @@ class ChannelChatBadges(BaseModel): class GlobalChatBadges(ChannelChatBadges): pass + + +class ChatSettingsData(BaseModel): + model_config = ConfigDict(extra='forbid') + + broadcaster_id: int + emote_mode: bool + follower_mode: bool + follower_mode_duration: int | None + moderator_id: int | None = None + non_moderator_chat_delay: bool | None = None + non_moderator_chat_delay_duration: int | None = None + slow_mode: bool + slow_mode_wait_time: int | None + subscriber_mode: bool + unique_chat_mode: bool + + +class ChatSettings(BaseModel): + model_config = ConfigDict(extra='forbid') + + data: list[ChatSettingsData] -- 2.47.2 From a95ddc36d446f382985906496b2270d0c756fed5 Mon Sep 17 00:00:00 2001 From: Miwory Date: Thu, 11 Dec 2025 17:10:29 +0300 Subject: [PATCH 10/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=BE=20get=5Fshared=5Fchat=5Fsession?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/twitchclient/api.py | 27 +++++++++++++++++++++++++++ src/twitchclient/schema.py | 22 ++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/src/twitchclient/api.py b/src/twitchclient/api.py index 9852396..05f9dff 100644 --- a/src/twitchclient/api.py +++ b/src/twitchclient/api.py @@ -913,3 +913,30 @@ class TwitchAPIClient(AioHTTPXClient): case _: raise s.Error(req.status_code, 'Internal Server Error') + + async def get_shared_chat_session( + self, + access_token: str, + broadcaster_id: int | str, + cache_time: int | None = None, + ): + req = await self.get( + '/shared_chat/session', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.SharedChatSession.model_validate(req.json()).data + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.Error(req.status_code, req.json()['message']) + + case _: + raise s.Error(req.status_code, 'Internal Server Error') diff --git a/src/twitchclient/schema.py b/src/twitchclient/schema.py index a72aa20..fc05553 100644 --- a/src/twitchclient/schema.py +++ b/src/twitchclient/schema.py @@ -563,3 +563,25 @@ class ChatSettings(BaseModel): model_config = ConfigDict(extra='forbid') data: list[ChatSettingsData] + + +class SharedChatSessionParticipant(BaseModel): + model_config = ConfigDict(extra='forbid') + + broadcaster_id: int + + +class SharedChatSessionData(BaseModel): + model_config = ConfigDict(extra='forbid') + + session_id: str + host_broadcaster_id: int + participants: list[SharedChatSessionParticipant] + created_at: datetime + updated_at: datetime + + +class SharedChatSession(BaseModel): + model_config = ConfigDict(extra='forbid') + + data: list[SharedChatSessionData] -- 2.47.2 From 9d80ad7d694717a0fd53e39c2805a27562e70036 Mon Sep 17 00:00:00 2001 From: Miwory Date: Thu, 11 Dec 2025 17:14:35 +0300 Subject: [PATCH 11/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20get=5Fuser=5Femotes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/twitchclient/api.py | 35 +++++++++++++++++++++++++++++++++++ src/twitchclient/schema.py | 21 ++++++++++++++++++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/twitchclient/api.py b/src/twitchclient/api.py index 05f9dff..941e7f8 100644 --- a/src/twitchclient/api.py +++ b/src/twitchclient/api.py @@ -940,3 +940,38 @@ class TwitchAPIClient(AioHTTPXClient): case _: raise s.Error(req.status_code, 'Internal Server Error') + + async def get_user_emotes( + self, + access_token: str, + user_id: int | str, + after: str | None = None, + broadcaster_id: int | str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/chat/emotes/global', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'user_id': user_id, + 'after': after, + 'broadcaster_id': broadcaster_id, + } + ), + ) + + match req.status_code: + case st.OK: + return s.UserEmotes.model_validate(req.json()) + + case st.UNAUTHORIZED: + raise s.Error(req.status_code, req.json()['message']) + + case _: + raise s.Error(req.status_code, 'Internal Server Error') diff --git a/src/twitchclient/schema.py b/src/twitchclient/schema.py index fc05553..3b533b6 100644 --- a/src/twitchclient/schema.py +++ b/src/twitchclient/schema.py @@ -492,7 +492,22 @@ class ChannelEmote(BaseModel): name: str images: ChannelEmoteImages tier: int - emote_type: Literal['bitstier', 'follower', 'subscriptions'] + emote_type: Literal[ + 'none', + 'bitstier', + 'follower', + 'subscriptions', + 'channelpoints', + 'rewards', + 'hypetrain', + 'prime', + 'turbo', + 'smilies', + 'globals', + 'owl2019', + 'twofactor', + 'limitedtime' + ] format: list[Literal['animated', 'static']] scale: list[Literal['1.0', '2.0', '3.0']] theme_mode: list[Literal['dark', 'light']] @@ -585,3 +600,7 @@ class SharedChatSession(BaseModel): model_config = ConfigDict(extra='forbid') data: list[SharedChatSessionData] + + +class UserEmotes(ChannelEmotes): + pagination: Pagination | dict[Any, Any] | None = None -- 2.47.2 From e6c99a676f8d8a072d69c6cd75beb7d22b88f319 Mon Sep 17 00:00:00 2001 From: Miwory Date: Thu, 11 Dec 2025 17:22:40 +0300 Subject: [PATCH 12/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20send=5Fchat=5Fannouncement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/twitchclient/api.py | 80 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/src/twitchclient/api.py b/src/twitchclient/api.py index 941e7f8..54830df 100644 --- a/src/twitchclient/api.py +++ b/src/twitchclient/api.py @@ -975,3 +975,83 @@ class TwitchAPIClient(AioHTTPXClient): case _: raise s.Error(req.status_code, 'Internal Server Error') + + async def update_chat_settings( + self, + access_token: str, + broadcaster_id: int | str, + moderator_id: int | str, + *, + emote_mode: bool | None = None, + follower_mode: bool | None = None, + follower_mode_duration: int | None = None, + non_moderator_chat_delay: bool | None = None, + non_moderator_chat_delay_duration: Literal[2, 4, 6] | None = None, + slow_mode: bool | None = None, + slow_mode_wait_time: int | None = None, + subscriber_mode: bool | None = None, + unique_chat_mode: bool | None = None, + ): + req = await self.put( + '/chat/settings', + headers={'Authorization': f'Bearer {access_token}'}, + params={ + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + }, + json=self.clean_dict( + { + 'emote_mode': emote_mode, + 'follower_mode': follower_mode, + 'follower_mode_duration': follower_mode_duration, + 'non_moderator_chat_delay': non_moderator_chat_delay, + 'non_moderator_chat_delay_duration': ( + non_moderator_chat_delay_duration + ), + 'slow_mode': slow_mode, + 'slow_mode_wait_time': slow_mode_wait_time, + 'subscriber_mode': subscriber_mode, + 'unique_chat_mode': unique_chat_mode, + } + ), + ) + + match req.status_code: + case st.OK: + return s.ChatSettings.model_validate(req.json()).data + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.Error(req.status_code, req.json()['message']) + + case _: + raise s.Error(req.status_code, 'Internal Server Error') + + async def send_chat_announcement( + self, + access_token: str, + broadcaster_id: int | str, + moderator_id: int | str, + message: str, + color: Literal[ + 'blue', 'green', 'orange', 'purple', 'primary' + ] = 'primary', + ): + req = await self.post( + '/chat/announcements', + headers={'Authorization': f'Bearer {access_token}'}, + params={ + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + }, + json=self.clean_dict({'message': message, 'color': color}), + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: + raise s.Error(req.status_code, req.json()['message']) + + case _: + raise s.Error(req.status_code, 'Internal Server Error') -- 2.47.2 From b279a6c977f7ebb8b11f4b13f87bf720573a2e69 Mon Sep 17 00:00:00 2001 From: Miwory Date: Thu, 11 Dec 2025 17:30:02 +0300 Subject: [PATCH 13/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20send=5Fshoutout,=20send=5Fchat=5Fmessage,?= =?UTF-8?q?=20get=5Fuser=5Fchat=5Fcolor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/twitchclient/api.py | 90 ++++++++++++++++++++++++++++++++++++++ src/twitchclient/schema.py | 36 +++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/src/twitchclient/api.py b/src/twitchclient/api.py index 54830df..1468f6c 100644 --- a/src/twitchclient/api.py +++ b/src/twitchclient/api.py @@ -1055,3 +1055,93 @@ class TwitchAPIClient(AioHTTPXClient): case _: raise s.Error(req.status_code, 'Internal Server Error') + + async def send_shoutout( + self, + access_token: str, + from_broadcaster_id: int | str, + to_broadcaster_id: int | str, + moderator_id: int | str, + ): + req = await self.post( + '/chat/shoutouts', + headers={'Authorization': f'Bearer {access_token}'}, + params={ + 'from_broadcaster_id': from_broadcaster_id, + 'to_broadcaster_id': to_broadcaster_id, + 'moderator_id': moderator_id, + }, + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.TOO_MANY_REQUESTS + ): + raise s.Error(req.status_code, req.json()['message']) + + case _: + raise s.Error(req.status_code, 'Internal Server Error') + + async def send_chat_message( + self, + access_token: str, + broadcaster_id: int | str, + sender_id: int | str, + message: str, + *, + reply_parent_message_id: str | None = None, + for_source_only: bool | None = None, + ): + req = await self.post( + '/chat/messages', + headers={'Authorization': f'Bearer {access_token}'}, + json=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'sender_id': sender_id, + 'reply_parent_message_id': reply_parent_message_id, + 'for_source_only': for_source_only, + 'message': message, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Message.model_validate(req.json()).data + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.TOO_MANY_REQUESTS + ): + raise s.Error(req.status_code, req.json()['message']) + + case _: + raise s.Error(req.status_code, 'Internal Server Error') + + async def get_user_chat_color( + self, access_token: str, user_id: int | str | list[int] | list[str] + ): + req = await self.get( + '/chat/colors', + headers={'Authorization': f'Bearer {access_token}'}, + params={'user_id': user_id}, + ) + + match req.status_code: + case st.OK: + return s.UserChatColor.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.Error(req.status_code, req.json()['message']) + + case _: + raise s.Error(req.status_code, 'Internal Server Error') diff --git a/src/twitchclient/schema.py b/src/twitchclient/schema.py index 3b533b6..99aedcb 100644 --- a/src/twitchclient/schema.py +++ b/src/twitchclient/schema.py @@ -604,3 +604,39 @@ class SharedChatSession(BaseModel): class UserEmotes(ChannelEmotes): pagination: Pagination | dict[Any, Any] | None = None + + +class MessageDropReason(BaseModel): + model_config = ConfigDict(extra='forbid') + + code: str + message: str + + +class MessageData(BaseModel): + model_config = ConfigDict(extra='forbid') + + message_id: str + is_sent: bool + drop_reason: MessageDropReason | None = None + + +class Message(BaseModel): + model_config = ConfigDict(extra='forbid') + + data: list[MessageData] + + +class UserChatColorData(BaseModel): + model_config = ConfigDict(extra='forbid') + + user_id: int + user_login: str + user_name: str + color: str + + +class UserChatColor(BaseModel): + model_config = ConfigDict(extra='forbid') + + data: list[UserChatColorData] -- 2.47.2 From bb99154131ed22c6ec281649b9b3f758616f4149 Mon Sep 17 00:00:00 2001 From: Miwory Date: Thu, 11 Dec 2025 19:06:43 +0300 Subject: [PATCH 14/15] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20update=5Fuser=5Fchat=5Fcolor,=20create=5Fc?= =?UTF-8?q?lip,=20get=5Fclips?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/twitchclient/api.py | 91 ++++++++++++++++++++++++++++++++++++++ src/twitchclient/schema.py | 29 ++++++++++++ 2 files changed, 120 insertions(+) diff --git a/src/twitchclient/api.py b/src/twitchclient/api.py index 1468f6c..871ad98 100644 --- a/src/twitchclient/api.py +++ b/src/twitchclient/api.py @@ -1145,3 +1145,94 @@ class TwitchAPIClient(AioHTTPXClient): case _: raise s.Error(req.status_code, 'Internal Server Error') + + async def update_user_chat_color( + self, + access_token: str, + user_id: int | str, + color: str, + ): + req = await self.put( + '/chat/color', + headers={'Authorization': f'Bearer {access_token}'}, + params={'user_id': user_id, 'color': color}, + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.Error(req.status_code, req.json()['message']) + + case _: + raise s.Error(req.status_code, 'Internal Server Error') + + async def create_clip( + self, + access_token: str, + broadcaster_id: int | str, + *, + has_delay: bool | None = None, + ): + req = await self.post( + '/clips', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + {'broadcaster_id': broadcaster_id, 'has_delay': has_delay} + ), + ) + + match req.status_code: + case st.ACCEPTED: + return True + + case ( + st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN | st.NOT_FOUND + ): + raise s.Error(req.status_code, req.json()['message']) + + case _: + raise s.Error(req.status_code, 'Internal Server Error') + + async def get_clips( + self, + access_token: str, + broadcaster_id: int | str | None = None, + game_id: int | str | None = None, + clip_id: str | list[str] | None = None, + started_at: datetime | None = None, + ended_at: datetime | None = None, + first: int = 20, + before: str | None = None, + after: str | None = None, + *, + is_featured: bool | None = None, + ): + req = await self.get( + '/clips', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'game_id': game_id, + 'clip_id': clip_id, + 'started_at': started_at, + 'ended_at': ended_at, + 'first': first, + 'before': before, + 'after': after, + 'is_featured': is_featured, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Clips.model_validate(req.json()).data + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.Error(req.status_code, req.json()['message']) + + case _: + raise s.Error(req.status_code, 'Internal Server Error') diff --git a/src/twitchclient/schema.py b/src/twitchclient/schema.py index 99aedcb..bdde561 100644 --- a/src/twitchclient/schema.py +++ b/src/twitchclient/schema.py @@ -640,3 +640,32 @@ class UserChatColor(BaseModel): model_config = ConfigDict(extra='forbid') data: list[UserChatColorData] + + +class Clip(BaseModel): + model_config = ConfigDict(extra='forbid') + + id: str + url: str + embed_url: str + broadcaster_id: int + broadcaster_name: str + creator_id: int + creator_name: str + video_id: str + game_id: int + language: str + title: str + view_count: int + created_at: datetime + thumbnail_url: str + duration: float + vod_offset: int | None + is_featured: bool + + +class Clips(BaseModel): + model_config = ConfigDict(extra='forbid') + + data: list[Clip] + pagination: Pagination | dict[Any, Any] | None = None -- 2.47.2 From fbc628bc3bdef65141c16b398724e67bbbbac097 Mon Sep 17 00:00:00 2001 From: Miwory Date: Wed, 17 Dec 2025 06:30:13 +0300 Subject: [PATCH 15/15] =?UTF-8?q?=D0=A0=D0=B5=D0=BB=D0=B8=D0=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pyproject.toml | 15 +- src/twitchclient/api.py | 3152 +++++++++++++++++++- src/twitchclient/auth.py | 126 + src/twitchclient/eventsub/__init__.py | 0 src/twitchclient/eventsub/conditions.py | 51 + src/twitchclient/eventsub/statuses.py | 43 + src/twitchclient/eventsub/subscriptions.py | 564 ++++ src/twitchclient/eventsub/transports.py | 22 + src/twitchclient/eventsub/types.py | 201 ++ src/twitchclient/schema.py | 1238 ++++++-- src/twitchclient/scopes.py | 168 ++ 11 files changed, 5246 insertions(+), 334 deletions(-) create mode 100644 src/twitchclient/auth.py create mode 100644 src/twitchclient/eventsub/__init__.py create mode 100644 src/twitchclient/eventsub/conditions.py create mode 100644 src/twitchclient/eventsub/statuses.py create mode 100644 src/twitchclient/eventsub/subscriptions.py create mode 100644 src/twitchclient/eventsub/transports.py create mode 100644 src/twitchclient/eventsub/types.py create mode 100644 src/twitchclient/scopes.py diff --git a/pyproject.toml b/pyproject.toml index 48c1d6d..cc82856 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "twitchclient" -version = "0.2.0" +version = "1.0.0" description = "Client for Twitch API" readme = "README.md" authors = [ @@ -18,10 +18,10 @@ build-backend = "uv_build" [project.optional-dependencies] dev = [ - "ruff==0.14.2", - "pyright==1.1.406", - "poethepoet==0.37.0", - "pre-commit==4.3.0", + "ruff==0.14.9", + "pyright==1.1.407", + "poethepoet==0.38.0", + "pre-commit==4.5.0", ] [[tool.uv.index]] @@ -41,6 +41,10 @@ _lint = "pre-commit run --all-files" lint = ["_git", "_lint"] check = "uv pip ls --outdated" +major = "uv version --bump major" +minor = "uv version --bump minor" +patch = "uv version --bump patch" + [tool.pyright] venvPath = "." venv = ".venv" @@ -50,6 +54,7 @@ strictSetInference = true deprecateTypingAliases = true typeCheckingMode = "strict" pythonPlatform = "All" +stubPath = "typings" [tool.ruff] target-version = "py313" diff --git a/src/twitchclient/api.py b/src/twitchclient/api.py index 871ad98..f3cbade 100644 --- a/src/twitchclient/api.py +++ b/src/twitchclient/api.py @@ -1,10 +1,13 @@ from datetime import datetime from typing import Literal +from zoneinfo import ZoneInfo from aiohttpx import status as st from aiohttpx.client import AioHTTPXClient from . import schema as s +from .eventsub import statuses as sub_status +from .eventsub import types as sub_type class TwitchAPIClient(AioHTTPXClient): @@ -25,13 +28,11 @@ class TwitchAPIClient(AioHTTPXClient): headers={'Client-Id': self.client_id}, redis_url=redis_url, key='twitch', - limit=10, + limit=700, logger='Twitch API', ) - async def start_commercial( - self, access_token: str, broadcaster_id: int | str - ): + async def start_commercial(self, access_token: str, broadcaster_id: int): req = await self.get( '/channels/commercial', headers={'Authorization': f'Bearer {access_token}'}, @@ -42,7 +43,7 @@ class TwitchAPIClient(AioHTTPXClient): match req.status_code: case st.OK: - return s.StartCommercial.model_validate(req.json()).data + return s.StartCommercial.model_validate(req.json()) case ( st.BAD_REQUEST @@ -50,14 +51,12 @@ class TwitchAPIClient(AioHTTPXClient): | st.NOT_FOUND | st.TOO_MANY_REQUESTS ): - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') - async def get_ad_schedule( - self, access_token: str, broadcaster_id: int | str - ): + async def get_ad_schedule(self, access_token: str, broadcaster_id: int): req = await self.get( '/channels/ads', headers={'Authorization': f'Bearer {access_token}'}, @@ -71,14 +70,12 @@ class TwitchAPIClient(AioHTTPXClient): return s.AdSchedule.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') - async def snooze_next_ad( - self, access_token: str, broadcaster_id: int | str - ): + async def snooze_next_ad(self, access_token: str, broadcaster_id: int): req = await self.post( '/channels/ads/schedule/snooze', headers={'Authorization': f'Bearer {access_token}'}, @@ -92,14 +89,15 @@ class TwitchAPIClient(AioHTTPXClient): return s.SnoozeNextAd.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_extension_analytics( self, access_token: str, + *, extension_id: str | None = None, analytics_type: Literal['overview_v2'] | None = None, started_at: datetime | None = None, @@ -127,14 +125,15 @@ class TwitchAPIClient(AioHTTPXClient): return s.ExtensionAnalytics.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_game_analytics( self, access_token: str, + *, game_id: int | None = None, analytics_type: Literal['overview_v2'] | None = None, started_at: datetime | None = None, @@ -162,16 +161,17 @@ class TwitchAPIClient(AioHTTPXClient): return s.GameAnalytics.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_bits_leaderboard( self, access_token: str, count: int = 10, period: Literal['day', 'week', 'month', 'year', 'all'] = 'all', + *, started_at: datetime | None = None, user_id: int | None = None, ): @@ -193,13 +193,13 @@ class TwitchAPIClient(AioHTTPXClient): return s.BitsLeaderboard.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_cheermotes( - self, access_token: str, broadcaster_id: int | str | None = None + self, access_token: str, *, broadcaster_id: int | None = None ): req = await self.get( '/bits/cheermotes', @@ -212,15 +212,16 @@ class TwitchAPIClient(AioHTTPXClient): return s.Cheermotes.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_extension_transactions( self, access_token: str, extension_id: str, + *, user_id: int | list[int] | None = None, first: int = 20, after: str | None = None, @@ -243,15 +244,15 @@ class TwitchAPIClient(AioHTTPXClient): return s.ExtensionTransactions.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_channel_information( self, access_token: str, - broadcaster_id: int | list[int] | str | list[str], + broadcaster_id: int | list[int], ): req = await self.get( '/channels', @@ -264,17 +265,17 @@ class TwitchAPIClient(AioHTTPXClient): return s.ChannelsInformation.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def modify_channel_information( self, access_token: str, - broadcaster_id: int | str, + broadcaster_id: int, *, - game_id: int | str | None = None, + game_id: int | None = None, broadcaster_language: str | None = None, title: str | None = None, delay: int | None = None, @@ -311,13 +312,13 @@ class TwitchAPIClient(AioHTTPXClient): | st.FORBIDDEN | st.TOO_MANY_REQUESTS ): - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_channel_editors( - self, access_token: str, broadcaster_id: int | str + self, access_token: str, broadcaster_id: int ): req = await self.get( '/channels/editors', @@ -330,15 +331,15 @@ class TwitchAPIClient(AioHTTPXClient): return s.ChannelEditors.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_followed_channels( self, access_token: str, - broadcaster_id: int | str, + broadcaster_id: int, *, first: int = 20, after: str | None = None, @@ -360,15 +361,15 @@ class TwitchAPIClient(AioHTTPXClient): return s.FollowedChannels.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_channel_followers( self, access_token: str, - broadcaster_id: int | str, + broadcaster_id: int, *, user_id: int | None = None, first: int = 20, @@ -392,15 +393,15 @@ class TwitchAPIClient(AioHTTPXClient): return s.ChannelFollowers.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def create_custom_rewards( self, access_token: str, - broadcaster_id: int | str, + broadcaster_id: int, title: str, cost: int, *, @@ -453,13 +454,13 @@ class TwitchAPIClient(AioHTTPXClient): | st.FORBIDDEN | st.TOO_MANY_REQUESTS ): - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def delete_custom_reward( - self, access_token: str, broadcaster_id: int | str, reward_id: str + self, access_token: str, broadcaster_id: int, reward_id: str ): req = await self.delete( '/channel_points/custom_rewards', @@ -478,15 +479,15 @@ class TwitchAPIClient(AioHTTPXClient): | st.NOT_FOUND | st.TOO_MANY_REQUESTS ): - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_custom_rewards( self, access_token: str, - broadcaster_id: int | str, + broadcaster_id: int, *, reward_id: str | None = None, only_manageable_rewards: bool = False, @@ -514,15 +515,15 @@ class TwitchAPIClient(AioHTTPXClient): | st.NOT_FOUND | st.TOO_MANY_REQUESTS ): - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_custom_reward_redemption( self, access_token: str, - broadcaster_id: int | str, + broadcaster_id: int, reward_id: str, status: Literal['CANCELED', 'FULFILLED', 'UNFULFILLED'], *, @@ -559,15 +560,15 @@ class TwitchAPIClient(AioHTTPXClient): | st.NOT_FOUND | st.TOO_MANY_REQUESTS ): - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def update_custom_reward( self, access_token: str, - broadcaster_id: int | str, + broadcaster_id: int, reward_id: str, *, title: str | None = None, @@ -623,15 +624,15 @@ class TwitchAPIClient(AioHTTPXClient): | st.NOT_FOUND | st.TOO_MANY_REQUESTS ): - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def update_redemption_status( self, access_token: str, - broadcaster_id: int | str, + broadcaster_id: int, reward_id: int, redemption_id: int | list[int], status: Literal['CANCELED', 'FULFILLED'], @@ -658,13 +659,13 @@ class TwitchAPIClient(AioHTTPXClient): | st.NOT_FOUND | st.TOO_MANY_REQUESTS ): - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_charity_campaign( - self, access_token: str, broadcaster_id: int | str + self, access_token: str, broadcaster_id: int ): req = await self.get( '/charity/campaigns', @@ -677,15 +678,16 @@ class TwitchAPIClient(AioHTTPXClient): return s.CharityCampaign.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_charity_campaign_donations( self, access_token: str, - broadcaster_id: int | str, + broadcaster_id: int, + *, first: int = 20, after: str | None = None, cache_time: int | None = None, @@ -712,16 +714,17 @@ class TwitchAPIClient(AioHTTPXClient): return s.CharityDonations.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_chatters( self, access_token: str, - broadcaster_id: int | str, + broadcaster_id: int, moderator_id: int, + *, first: int = 20, after: str | None = None, cache_time: int | None = None, @@ -749,15 +752,16 @@ class TwitchAPIClient(AioHTTPXClient): return s.Chatters.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_channel_emotes( self, access_token: str, - broadcaster_id: int | str, + broadcaster_id: int, + *, cache_time: int | None = None, ): req = await self.get( @@ -776,13 +780,13 @@ class TwitchAPIClient(AioHTTPXClient): return s.ChannelEmotes.model_validate(req.json()) case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_global_emotes( - self, access_token: str, cache_time: int | None = None + self, access_token: str, *, cache_time: int | None = None ): req = await self.get( '/chat/emotes/global', @@ -799,15 +803,16 @@ class TwitchAPIClient(AioHTTPXClient): return s.GlobalEmotes.model_validate(req.json()) case st.UNAUTHORIZED: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_emote_sets( self, access_token: str, emote_set_id: int, + *, cache_time: int | None = None, ): req = await self.get( @@ -826,15 +831,16 @@ class TwitchAPIClient(AioHTTPXClient): return s.EmoteSets.model_validate(req.json()) case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_channel_chat_badges( self, access_token: str, - broadcaster_id: int | str, + broadcaster_id: int, + *, cache_time: int | None = None, ): req = await self.get( @@ -853,13 +859,13 @@ class TwitchAPIClient(AioHTTPXClient): return s.ChannelChatBadges.model_validate(req.json()) case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_global_chat_badges( - self, access_token: str, cache_time: int | None = None + self, access_token: str, *, cache_time: int | None = None ): req = await self.get( '/chat/badges/global', @@ -876,16 +882,17 @@ class TwitchAPIClient(AioHTTPXClient): return s.GlobalChatBadges.model_validate(req.json()) case st.UNAUTHORIZED: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_chat_settings( self, access_token: str, - broadcaster_id: int | str, - moderator_id: int | str | None = None, + broadcaster_id: int, + *, + moderator_id: int | None = None, cache_time: int | None = None, ): req = await self.get( @@ -909,15 +916,16 @@ class TwitchAPIClient(AioHTTPXClient): return s.ChatSettings.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_shared_chat_session( self, access_token: str, - broadcaster_id: int | str, + broadcaster_id: int, + *, cache_time: int | None = None, ): req = await self.get( @@ -936,17 +944,18 @@ class TwitchAPIClient(AioHTTPXClient): return s.SharedChatSession.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_user_emotes( self, access_token: str, - user_id: int | str, + user_id: int, + *, after: str | None = None, - broadcaster_id: int | str | None = None, + broadcaster_id: int | None = None, cache_time: int | None = None, ): req = await self.get( @@ -971,16 +980,16 @@ class TwitchAPIClient(AioHTTPXClient): return s.UserEmotes.model_validate(req.json()) case st.UNAUTHORIZED: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def update_chat_settings( self, access_token: str, - broadcaster_id: int | str, - moderator_id: int | str, + broadcaster_id: int, + moderator_id: int, *, emote_mode: bool | None = None, follower_mode: bool | None = None, @@ -1021,16 +1030,16 @@ class TwitchAPIClient(AioHTTPXClient): return s.ChatSettings.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def send_chat_announcement( self, access_token: str, - broadcaster_id: int | str, - moderator_id: int | str, + broadcaster_id: int, + moderator_id: int, message: str, color: Literal[ 'blue', 'green', 'orange', 'purple', 'primary' @@ -1051,17 +1060,17 @@ class TwitchAPIClient(AioHTTPXClient): return True case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def send_shoutout( self, access_token: str, - from_broadcaster_id: int | str, - to_broadcaster_id: int | str, - moderator_id: int | str, + from_broadcaster_id: int, + to_broadcaster_id: int, + moderator_id: int, ): req = await self.post( '/chat/shoutouts', @@ -1083,16 +1092,16 @@ class TwitchAPIClient(AioHTTPXClient): | st.FORBIDDEN | st.TOO_MANY_REQUESTS ): - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def send_chat_message( self, access_token: str, - broadcaster_id: int | str, - sender_id: int | str, + broadcaster_id: int, + sender_id: int, message: str, *, reply_parent_message_id: str | None = None, @@ -1122,17 +1131,26 @@ class TwitchAPIClient(AioHTTPXClient): | st.FORBIDDEN | st.TOO_MANY_REQUESTS ): - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_user_chat_color( - self, access_token: str, user_id: int | str | list[int] | list[str] + self, + access_token: str, + user_id: int | list[int], + *, + cache_time: int | None = None, ): req = await self.get( '/chat/colors', - headers={'Authorization': f'Bearer {access_token}'}, + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), params={'user_id': user_id}, ) @@ -1141,15 +1159,15 @@ class TwitchAPIClient(AioHTTPXClient): return s.UserChatColor.model_validate(req.json()) case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def update_user_chat_color( self, access_token: str, - user_id: int | str, + user_id: int, color: str, ): req = await self.put( @@ -1163,15 +1181,15 @@ class TwitchAPIClient(AioHTTPXClient): return True case st.BAD_REQUEST | st.UNAUTHORIZED: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def create_clip( self, access_token: str, - broadcaster_id: int | str, + broadcaster_id: int, *, has_delay: bool | None = None, ): @@ -1190,28 +1208,34 @@ class TwitchAPIClient(AioHTTPXClient): case ( st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN | st.NOT_FOUND ): - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') async def get_clips( self, access_token: str, - broadcaster_id: int | str | None = None, - game_id: int | str | None = None, + *, + broadcaster_id: int | None = None, + game_id: int | None = None, clip_id: str | list[str] | None = None, started_at: datetime | None = None, ended_at: datetime | None = None, first: int = 20, before: str | None = None, after: str | None = None, - *, is_featured: bool | None = None, + cache_time: int | None = None, ): req = await self.get( '/clips', - headers={'Authorization': f'Bearer {access_token}'}, + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), params=self.clean_dict( { 'broadcaster_id': broadcaster_id, @@ -1232,7 +1256,2877 @@ class TwitchAPIClient(AioHTTPXClient): return s.Clips.model_validate(req.json()).data case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: - raise s.Error(req.status_code, req.json()['message']) + raise s.ClientError(req.status_code, req.json()['message']) case _: - raise s.Error(req.status_code, 'Internal Server Error') + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_clips_downloads( + self, + access_token: str, + editor_id: int, + broadcaster_id: int, + clip_id: str, + *, + cache_time: int | None = None, + ): + req = await self.get( + '/clips/downloads', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'editor_id': editor_id, + 'broadcaster_id': broadcaster_id, + 'clip_id': clip_id, + } + ), + ) + + match req.status_code: + case st.OK: + return s.ClipsDownloads.model_validate(req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.INTERNAL_SERVER_ERROR + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_conduits( + self, access_token: str, *, cache_time: int | None = None + ): + req = await self.get( + '/eventsub/conduits', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Conduits.model_validate(req.json()) + + case st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def create_conduits(self, access_token: str, shard_count: int): + req = await self.post( + '/eventsub/conduits', + headers={'Authorization': f'Bearer {access_token}'}, + json={'shard_count': shard_count}, + ) + + match req.status_code: + case st.OK: + return s.Conduits.model_validate(req.json()) + + case st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def update_conduits( + self, access_token: str, conduit_id: str, shard_count: int + ): + req = await self.patch( + '/eventsub/conduits', + headers={'Authorization': f'Bearer {access_token}'}, + json={'shard_count': shard_count, 'id': conduit_id}, + ) + + match req.status_code: + case st.OK: + return s.Conduits.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def delete_conduit(self, access_token: str, conduit_id: str): + req = await self.delete( + '/eventsub/conduits', + headers={'Authorization': f'Bearer {access_token}'}, + params={'id': conduit_id}, + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_conduit_shards( + self, + access_token: str, + conduit_id: str, + *, + status: str | None = None, + after: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/eventsub/conduits/shards', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + {'id': conduit_id, 'status': status, 'after': after} + ), + ) + + match req.status_code: + case st.OK: + return s.ConduitShards.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def update_conduit_shards( + self, + access_token: str, + conduit_id: str, + shards: list[s.UpdateConduitShard], + ): + req = await self.patch( + '/eventsub/conduits/shards', + headers={'Authorization': f'Bearer {access_token}'}, + json={'conduit_id': conduit_id, 'shards': shards}, + ) + + match req.status_code: + case st.ACCEPTED: + return s.ConduitShards.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_content_classification_labels( + self, + access_token: str, + locale: Literal[ + 'bg-BG', + 'cs-CZ', + 'da-DK', + 'de-DE', + 'el-GR', + 'en-GB', + 'en-US', + 'es-ES', + 'es-MX', + 'fi-FI', + 'fr-FR', + 'hu-HU', + 'it-IT', + 'ja-JP', + 'ko-KR', + 'nl-NL', + 'no-NO', + 'pl-PL', + 'pt-BR', + 'pt-PT', + 'ro-RO', + 'ru-RU', + 'sk-SK', + 'sv-SE', + 'th-TH', + 'tr-TR', + 'vi-VN', + 'zh-CN', + 'zh-TW', + ] = 'en-US', + ): + req = await self.get( + '/content_classification_labels', + headers={'Authorization': f'Bearer {access_token}'}, + params={'locale': locale}, + ) + + match req.status_code: + case st.OK: + return s.ContentClassificationLabels.model_validate(req.json()) + + case st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_drops_entitlements( + self, + access_token: str, + *, + drop_id: str | list[str] | None = None, + user_id: int | None = None, + game_id: int | None = None, + fulfillment_status: Literal['CLAIMED', 'FULFILLED'] | None = None, + after: str | None = None, + first: int = 20, + cache_time: int | None = None, + ): + req = await self.get( + '/entitlements/drops', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'drop_id': drop_id, + 'user_id': user_id, + 'game_id': game_id, + 'fulfillment_status': fulfillment_status, + 'after': after, + 'first': first, + } + ), + ) + + match req.status_code: + case st.OK: + return s.DropsEntitlements.model_validate(req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.INTERNAL_SERVER_ERROR + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def update_drops_entitlements( + self, + access_token: str, + *, + entitlement_ids: list[str] | None = None, + fulfillment_status: Literal['CLAIMED', 'FULFILLED'] | None = None, + ): + req = await self.patch( + '/entitlements/drops', + headers={'Authorization': f'Bearer {access_token}'}, + json=self.clean_dict( + { + 'entitlement_ids': entitlement_ids, + 'fulfillment_status': fulfillment_status, + } + ), + ) + + match req.status_code: + case st.OK: + return s.UpdateDropsEntitlements.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.INTERNAL_SERVER_ERROR: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_extension_configuration_segment( + self, + jwt_token: str, + *, + broadcaster_id: int | None = None, + extension_id: int | None = None, + segment: Literal['broadcaster', 'developer', 'global'] | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/extensions/configurations', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {jwt_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'extension_id': extension_id, + 'segment': segment, + } + ), + ) + + match req.status_code: + case st.OK: + return s.ExtensionConfigurationSegment.model_validate( + req.json() + ) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def set_extension_configuration_segment( + self, + jwt_token: str, + extension_id: str, + segment: Literal['broadcaster', 'developer', 'global'], + *, + broadcaster_id: int | None = None, + content: str | None = None, + version: str | None = None, + ): + req = await self.put( + '/extensions/configurations', + headers={'Authorization': f'Bearer {jwt_token}'}, + json=self.clean_dict( + { + 'extension_id': extension_id, + 'segment': segment, + 'broadcaster_id': broadcaster_id, + 'content': content, + 'version': version, + } + ), + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def set_extension_required_configurations( + self, + jwt_token: str, + broadcaster_id: int, + extension_id: str, + extension_version: str, + required_configuration: str, + ): + req = await self.put( + '/extensions/configurations/required', + headers={'Authorization': f'Bearer {jwt_token}'}, + json={ + 'broadcaster_id': broadcaster_id, + 'extension_id': extension_id, + 'extension_version': extension_version, + 'required_configuration': required_configuration, + }, + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def send_extension_pubsub_message( + self, + jwt_token: str, + target: list[str], + broadcaster_id: int, + message: str, + *, + is_global_broadcast: bool | None = None, + ): + req = await self.post( + '/extensions/pubsub', + headers={'Authorization': f'Bearer {jwt_token}'}, + json=self.clean_dict( + { + 'target': target, + 'broadcaster_id': broadcaster_id, + 'message': message, + 'is_global_broadcast': is_global_broadcast, + } + ), + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.UNPROCESSABLE_ENTITY: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_extension_live_channels( + self, + access_token: str, + extension_id: str, + *, + first: int | None = None, + after: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/extensions/live', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'extension_id': extension_id, + 'first': first, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.ExtensionLiveChannels.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_extension_secrets( + self, + jwt_token: str, + extension_id: str, + *, + cache_time: int | None = None, + ): + req = await self.get( + '/extensions/jwt/secrets', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {jwt_token}', + 'X-Cache-TTL': cache_time, + } + ), + params={'extension_id': extension_id}, + ) + + match req.status_code: + case st.OK: + return s.ExtensionSecrets.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def create_extension_secret( + self, jwt_token: str, extension_id: str, delay: int = 300 + ): + req = await self.post( + '/extensions/jwt/secrets', + headers={'Authorization': f'Bearer {jwt_token}'}, + params={'extension_id': extension_id, 'delay': delay}, + ) + + match req.status_code: + case st.OK: + return s.ExtensionSecrets.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def send_extension_chat_message( + self, + jwt_token: str, + broadcaster_id: int, + text: str, + extension_id: str, + extension_version: str, + ): + req = await self.post( + '/extensions/chat', + headers={'Authorization': f'Bearer {jwt_token}'}, + params={'broadcaster_id': broadcaster_id}, + json={ + 'text': text, + 'extension_id': extension_id, + 'extension_version': extension_version, + }, + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_extensions( + self, + jwt_token: str, + extension_id: str, + *, + extension_version: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/extensions', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {jwt_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'extension_id': extension_id, + 'extension_version': extension_version, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Extensions.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_released_extension( + self, + access_token: str, + extension_id: str, + *, + extension_version: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/extensions/released', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'extension_id': extension_id, + 'extension_version': extension_version, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Extensions.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_extension_bits_products( + self, + access_token: str, + *, + should_include_all: bool | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/bits/extensions', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'should_include_all': should_include_all, + } + ), + ) + + match req.status_code: + case st.OK: + return s.ExtensionBitsProducts.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def update_extension_bits_product( + self, + access_token: str, + sku: str, + cost_amount: int, + display_name: str, + *, + in_development: bool | None = None, + expiration: str | None = None, + is_broadcast: bool | None = None, + ): + req = await self.put( + '/bits/extensions', + headers={'Authorization': f'Bearer {access_token}'}, + json=self.clean_dict( + { + 'sku': sku, + 'cost': { + 'amount': cost_amount, + 'type': 'bits', + }, + 'display_name': display_name, + 'in_development': in_development, + 'expiration': expiration, + 'is_broadcast': is_broadcast, + } + ), + ) + + match req.status_code: + case st.NO_CONTENT: + return s.ExtensionBitsProducts.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def create_eventsub_subscription( + self, + access_token: str, + sub_type: sub_type.Any, + version: str | int, + condition: dict[str, str], + transport_method: Literal['webhook', 'websocket', 'conduit'], + *, + webhook_callback: str | None = None, + webhook_secret: str | None = None, + websocket_session_id: str | None = None, + conduit_id: str | None = None, + ): + req = await self.post( + '/eventsub/subscriptions', + headers={'Authorization': f'Bearer {access_token}'}, + json={ + 'type': sub_type, + 'version': version, + 'condition': condition, + 'transport': self.clean_dict( + { + 'method': transport_method, + 'callback': webhook_callback, + 'secret': webhook_secret, + 'session_id': websocket_session_id, + 'conduit_id': conduit_id, + } + ), + }, + ) + + match req.status_code: + case st.ACCEPTED: + return s.EventsubSubscription.model_validate(req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.CONFLICT + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def delete_eventsub_subscription( + self, access_token: str, sub_id: str + ): + req = await self.delete( + '/eventsub/subscriptions', + headers={'Authorization': f'Bearer {access_token}'}, + params={'id': sub_id}, + ) + + match req.status_code: + case st.NO_CONTENT: + return s.EventsubSubscription.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_eventsub_subscriptions( + self, + access_token: str, + *, + status: sub_status.Any | None = None, + sub_type: sub_type.Any | None = None, + user_id: int | None = None, + subscription_id: str | None = None, + after: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/eventsub/subscriptions', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'status': status, + 'type': sub_type, + 'user_id': user_id, + 'subscription_id': subscription_id, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.EventsubSubscriptions.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_top_games( + self, + access_token: str, + *, + first: int = 20, + after: str | None = None, + before: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/games/top', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'first': first, + 'after': after, + 'before': before, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Games.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_games( + self, + access_token: str, + game_id: int | list[int], + name: str, + *, + igdb_id: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/games', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'id': game_id, + 'name': name, + 'igdb_id': igdb_id, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Games.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_creator_goals( + self, + access_token: str, + broadcaster_id: int, + *, + cache_time: int | None = None, + ): + req = await self.get( + '/creator_goals', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.CreatorGoals.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + # TODO: implement guest star endpoints + + async def get_hype_train_status( + self, + access_token: str, + broadcaster_id: int, + *, + cache_time: int | None = None, + ): + req = await self.get( + '/hypetrain/status', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.HypeTrainStatus.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.INTERNAL_SERVER_ERROR: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def check_automod_status( + self, + access_token: str, + broadcaster_id: int, + *, + cache_time: int | None = None, + ): + req = await self.get( + '/moderation/enforcements/status', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.AutomodStatus.model_validate(req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def manage_held_automod_messages( + self, + access_token: str, + user_id: int, + msg_id: int, + action: Literal['ALLOW', 'DENY'], + ): + req = await self.post( + '/moderation/automod/message', + headers={'Authorization': f'Bearer {access_token}'}, + params={'action': action, 'msg_id': msg_id, 'user_id': user_id}, + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case ( + st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN | st.NOT_FOUND + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_automod_settings( + self, + access_token: str, + broadcaster_id: int, + moderator_id: int, + *, + cache_time: int | None = None, + ): + req = await self.get( + '/moderation/automod/settings', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params={ + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + }, + ) + + match req.status_code: + case st.OK: + return s.AutomodSettings.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def update_automod_settings( + self, + access_token: str, + broadcaster_id: int, + moderator_id: int, + *, + overall_level: int | None = None, + disability: int | None = None, + aggression: int | None = None, + sexuality_sex_or_gender: int | None = None, + misogyny: int | None = None, + bullying: int | None = None, + swearing: int | None = None, + race_ethnicity_or_religion: int | None = None, + sex_based_terms: int | None = None, + ): + req = await self.put( + '/moderation/automod/settings', + headers={'Authorization': f'Bearer {access_token}'}, + json=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + 'overall_level': overall_level, + 'disability': disability, + 'aggression': aggression, + 'sexuality_sex_or_gender': sexuality_sex_or_gender, + 'misogyny': misogyny, + 'bullying': bullying, + 'swearing': swearing, + 'race_ethnicity_or_religion': race_ethnicity_or_religion, + 'sex_based_terms': sex_based_terms, + } + ), + ) + + match req.status_code: + case st.NO_CONTENT: + return s.AutomodSettings.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_banned_users( + self, + access_token: str, + broadcaster_id: int, + user_id: int | list[int], + *, + first: int = 20, + after: str | None = None, + before: str | None = None, + ): + req = await self.get( + '/moderation/banned', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'user_id': user_id, + 'first': first, + 'after': after, + 'before': before, + } + ), + ) + + match req.status_code: + case st.OK: + return s.BannedUsers.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def ban_user( + self, + access_token: str, + broadcaster_id: int, + moderator_id: int, + user_id: int, + *, + duration: int | None = None, + reason: str | None = None, + ): + req = await self.post( + '/moderation/banned', + headers={'Authorization': f'Bearer {access_token}'}, + params={ + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + }, + json=self.clean_dict( + { + 'user_id': user_id, + 'duration': duration, + 'reason': reason, + } + ), + ) + + match req.status_code: + case st.OK: + return s.BanUser.model_validate(req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.CONFLICT + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def unban_user( + self, + access_token: str, + broadcaster_id: int, + moderator_id: int, + user_id: int, + ): + req = await self.delete( + '/moderation/bans', + headers={'Authorization': f'Bearer {access_token}'}, + params={ + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + 'user_id': user_id, + }, + ) + + match req.status_code: + case st.NO_CONTENT: + return + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.CONFLICT + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_unban_requests( + self, + access_token: str, + broadcaster_id: int, + moderator_id: int, + status: Literal[ + 'pending', 'approved', 'denied', 'acknowledged', 'canceled' + ], + *, + user_id: int | None = None, + after: str | None = None, + first: int | None = None, + ): + req = await self.get( + '/moderation/unbans', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + 'status': status, + 'user_id': user_id, + 'after': after, + 'first': first, + } + ), + ) + + match req.status_code: + case st.OK: + return s.UnbanRequests.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def resolve_unban_request( + self, + access_token: str, + broadcaster_id: int, + moderator_id: int, + unban_request_id: str, + status: Literal['approved', 'denied'], + *, + resolution_text: str | None = None, + ): + req = await self.patch( + '/moderation/unban_requests', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + 'unban_request_id': unban_request_id, + 'status': status, + 'resolution_text': resolution_text, + } + ), + ) + + match req.status_code: + case st.OK: + return s.UnbanRequests.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_blocked_terms( + self, + access_token: str, + broadcaster_id: int, + moderator_id: int, + *, + first: int | None = None, + after: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/moderation/blocked_terms', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + 'first': first, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.BlockedTerms.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def add_blocked_term( + self, + access_token: str, + broadcaster_id: int, + moderator_id: int, + text: str, + ): + req = await self.post( + '/moderation/blocked_terms', + headers={'Authorization': f'Bearer {access_token}'}, + params={ + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + 'text': text, + }, + ) + + match req.status_code: + case st.OK: + return s.BlockedTerms.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def remove_blocked_term( + self, + access_token: str, + broadcaster_id: int, + moderator_id: int, + blocked_term_id: str, + ): + req = await self.delete( + '/moderation/blocked_terms', + headers={'Authorization': f'Bearer {access_token}'}, + params={ + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + 'id': blocked_term_id, + }, + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def delete_chat_message( + self, + access_token: str, + broadcaster_id: int, + moderator_id: int, + *, + message_id: str | None = None, + ): + req = await self.delete( + '/moderation/chat', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + 'message_id': message_id, + } + ), + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case ( + st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN | st.NOT_FOUND + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_moderated_channels( + self, + access_token: str, + user_id: int, + *, + after: str | None = None, + first: int | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/moderation/channels', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'user_id': user_id, + 'after': after, + 'first': first, + } + ), + ) + + match req.status_code: + case st.OK: + return s.ModeratedChannels.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_moderators( + self, + access_token: str, + broadcaster_id: int, + *, + user_id: int | list[int] | None = None, + first: int = 20, + after: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/moderation/moderators', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'user_id': user_id, + 'first': first, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Moderators.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def add_channel_moderator( + self, access_token: str, broadcaster_id: int, user_id: int + ): + req = await self.post( + '/moderation/moderators', + headers={'Authorization': f'Bearer {access_token}'}, + params={'broadcaster_id': broadcaster_id, 'user_id': user_id}, + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.UNPROCESSABLE_ENTITY + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def remove_channel_moderator( + self, access_token: str, broadcaster_id: int, user_id: int + ): + req = await self.delete( + '/moderation/moderators', + headers={'Authorization': f'Bearer {access_token}'}, + params={'broadcaster_id': broadcaster_id, 'user_id': user_id}, + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_vips( + self, + access_token: str, + user_id: int, + broadcaster_id: int, + *, + first: int = 20, + after: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/channels/vips', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'user_id': user_id, + 'broadcaster_id': broadcaster_id, + 'first': first, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.VIPs.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def add_channel_vip( + self, access_token: str, broadcaster_id: int, user_id: int + ): + req = await self.post( + '/channels/vips', + headers={'Authorization': f'Bearer {access_token}'}, + params={'broadcaster_id': broadcaster_id, 'user_id': user_id}, + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.NOT_FOUND + | st.CONFLICT + | st.UNPROCESSABLE_ENTITY + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def remove_channel_vip( + self, access_token: str, broadcaster_id: int, user_id: int + ): + req = await self.delete( + '/channels/vips', + headers={'Authorization': f'Bearer {access_token}'}, + params={'broadcaster_id': broadcaster_id, 'user_id': user_id}, + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.NOT_FOUND + | st.UNPROCESSABLE_ENTITY + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def update_shield_mode_status( + self, + access_token: str, + broadcaster_id: int, + moderator_id: int, + *, + is_active: bool, + ): + req = await self.put( + '/moderation/shield_mode', + headers={'Authorization': f'Bearer {access_token}'}, + params={ + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + 'is_active': is_active, + }, + ) + + match req.status_code: + case st.OK: + return s.ShieldModeStatus.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_shield_mode_status( + self, + access_token: str, + broadcaster_id: int, + moderator_id: int, + *, + cache_time: int | None = None, + ): + req = await self.get( + '/moderation/shield_mode', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params={ + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + }, + ) + + match req.status_code: + case st.OK: + return s.ShieldModeStatus.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def warn_chat_user( + self, + access_token: str, + broadcaster_id: int, + moderator_id: int, + user_id: int, + reason: str, + ): + req = await self.post( + '/moderation/warnings', + headers={'Authorization': f'Bearer {access_token}'}, + params={ + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + }, + json={ + 'data': { + 'user_id': user_id, + 'reason': reason, + } + }, + ) + + match req.status_code: + case st.OK: + return s.UserWarns.model_validate(req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.CONFLICT + | st.TOO_MANY_REQUESTS + | st.INTERNAL_SERVER_ERROR + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_polls( + self, + access_token: str, + broadcaster_id: int, + *, + poll_id: int | list[int] | None = None, + first: int = 20, + after: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/polls', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'id': poll_id, + 'first': first, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Polls.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def create_poll( + self, + access_token: str, + broadcaster_id: int, + title: str, + choices: list[str], + duration: int, + *, + channel_points_voting_enabled: bool = False, + channel_points_per_vote: int | None = None, + ): + body = self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'title': title, + 'choices': [ + {'title': choice_title} for choice_title in choices + ], + 'duration': duration, + 'channel_points_voting_enabled': channel_points_voting_enabled, + 'channel_points_per_vote': channel_points_per_vote, + } + ) + req = await self.post( + '/polls', + headers={'Authorization': f'Bearer {access_token}'}, + json=body, + ) + + match req.status_code: + case st.OK: + return s.Polls.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def end_poll( + self, + access_token: str, + broadcaster_id: int, + poll_id: str, + status: Literal['TERMINATED', 'ARCHIVED'], + ): + req = await self.patch( + '/polls', + headers={'Authorization': f'Bearer {access_token}'}, + json={ + 'broadcaster_id': broadcaster_id, + 'id': poll_id, + 'status': status, + }, + ) + + match req.status_code: + case st.OK: + return s.Polls.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_predictions( + self, + access_token: str, + broadcaster_id: int, + *, + prediction_id: int | list[int] | None = None, + first: int = 20, + after: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/predictions', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'id': prediction_id, + 'first': first, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Predictions.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def create_prediction( + self, + access_token: str, + broadcaster_id: int, + title: str, + outcomes: list[str], + prediction_window: int, + ): + body = self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'title': title, + 'outcomes': [{'title': outcome} for outcome in outcomes], + 'prediction_window': prediction_window, + } + ) + req = await self.post( + '/predictions', + headers={'Authorization': f'Bearer {access_token}'}, + json=body, + ) + + match req.status_code: + case st.OK: + return s.Predictions.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def end_prediction( + self, + access_token: str, + broadcaster_id: int, + prediction_id: str, + status: Literal['RESOLVED', 'CANCELED', 'LOCKED'], + *, + winning_outcome_id: str | None = None, + ): + req = await self.patch( + '/predictions', + headers={'Authorization': f'Bearer {access_token}'}, + json=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'id': prediction_id, + 'status': status, + 'winning_outcome_id': winning_outcome_id, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Predictions.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def start_a_raid( + self, + access_token: str, + from_broadcaster_id: int, + to_broadcaster_id: int, + ): + req = await self.post( + '/raids', + headers={'Authorization': f'Bearer {access_token}'}, + json={ + 'from_broadcaster_id': from_broadcaster_id, + 'to_broadcaster_id': to_broadcaster_id, + }, + ) + + match req.status_code: + case st.OK: + return s.Raids.model_validate(req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.NOT_FOUND + | st.CONFLICT + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def cancel_a_raid(self, access_token: str, broadcaster_id: int): + req = await self.patch( + '/raids', + headers={'Authorization': f'Bearer {access_token}'}, + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.NOT_FOUND + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_channel_stream_schedule( + self, + access_token: str, + broadcaster_id: int, + *, + segment_id: int | list[int] | None = None, + start_time: datetime | None = None, + first: int = 20, + after: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/schedule', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'id': segment_id, + 'start_time': start_time, + 'first': first, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Schedules.model_validate(req.json()) + + case ( + st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN | st.NOT_FOUND + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_channel_icalendar( + self, + access_token: str, + broadcaster_id: int, + *, + cache_time: int | None = None, + ): + req = await self.get( + '/schedule/icalendar', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return req.content + + case st.BAD_REQUEST: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def update_channel_stream_schedule( + self, + access_token: str, + broadcaster_id: int, + *, + is_vacation_enabled: bool | None = None, + vacation_start_time: datetime | None = None, + vacation_end_time: datetime | None = None, + timezone: ZoneInfo | None = None, + ): + req = await self.patch( + '/schedule/settings', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'is_vacation_enabled': is_vacation_enabled, + 'vacation_start_time': vacation_start_time, + 'vacation_end_time': vacation_end_time, + 'timezone': str(timezone) if timezone else None, + } + ), + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def create_channel_stream_schedule_segment( + self, + access_token: str, + broadcaster_id: int, + start_time: datetime, + timezone: ZoneInfo, + duration: int, + *, + is_recurring: bool | None = None, + category_id: int | None, + title: str | None = None, + ): + req = await self.post( + '/schedule/segment', + headers={'Authorization': f'Bearer {access_token}'}, + params={'broadcaster_id': broadcaster_id}, + json=self.clean_dict( + { + 'start_time': start_time, + 'timezone': str(timezone), + 'duration': duration, + 'is_recurring': is_recurring, + 'category_id': category_id, + 'title': title, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Schedules.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def update_channel_stream_schedule_segment( + self, + access_token: str, + broadcaster_id: int, + segment_id: str, + *, + start_time: datetime | None = None, + duration: int | None = None, + category_id: int | None = None, + title: str | None = None, + is_canceled: bool | None = None, + timezone: ZoneInfo | None = None, + ): + req = await self.patch( + '/schedule/segment', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'id': segment_id, + } + ), + json=self.clean_dict( + { + 'start_time': start_time, + 'duration': duration, + 'category_id': category_id, + 'title': title, + 'is_canceled': is_canceled, + 'timezone': str(timezone) if timezone else None, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Schedules.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def delete_channel_stream_schedule_segment( + self, access_token: str, broadcaster_id: int, segment_id: str + ): + req = await self.delete( + '/schedule/segment', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'id': segment_id, + } + ), + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def search_categories( + self, + access_token: str, + query: str, + *, + first: int = 20, + after: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/search/categories', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + {'query': query, 'first': first, 'after': after} + ), + ) + + match req.status_code: + case st.OK: + return s.Categories.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def search_channels( + self, + access_token: str, + query: str, + *, + live_only: bool = False, + first: int = 20, + after: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/search/channels', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'query': query, + 'live_only': live_only, + 'first': first, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Channels.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_stream_key( + self, + access_token: str, + broadcaster_id: int, + *, + cache_time: int | None = None, + ): + req = await self.get( + '/streams/key', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.StreamKeys.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_streams( + self, + access_token: str, + *, + user_id: int | list[int] | None = None, + user_login: str | list[str] | None = None, + game_id: int | list[int] | None = None, + stream_type: Literal['live', 'all'] = 'all', + language: str | None = None, + first: int = 20, + before: str | None = None, + after: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/streams', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'user_id': user_id, + 'user_login': user_login, + 'game_id': game_id, + 'type': stream_type, + 'language': language, + 'first': first, + 'before': before, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Streams.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_followed_streams( + self, + access_token: str, + user_id: int, + *, + first: int = 20, + after: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/streams/followed', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'user_id': user_id, + 'first': first, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Streams.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def create_stream_marker( + self, + access_token: str, + user_id: int, + *, + description: str | None = None, + ): + req = await self.post( + '/streams/markers', + headers={ + 'Authorization': f'Bearer {access_token}', + }, + json=self.clean_dict( + { + 'user_id': user_id, + 'description': description, + } + ), + ) + + match req.status_code: + case st.OK: + return s.CreateStreamMarkers.model_validate(req.json()) + + case ( + st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN | st.NOT_FOUND + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_stream_markers( + self, + access_token: str, + user_id: int, + video_id: int, + *, + first: int = 20, + before: str | None = None, + after: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/streams/markers', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'user_id': user_id, + 'video_id': video_id, + 'first': first, + 'before': before, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.StreamMarkers.model_validate(req.json()) + + case ( + st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN | st.NOT_FOUND + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_broadcaster_subscriptions( + self, + access_token: str, + broadcaster_id: int, + *, + first: int = 20, + after: str | None = None, + before: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/subscriptions', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'first': first, + 'after': after, + 'before': before, + } + ), + ) + + match req.status_code: + case st.OK: + return s.BroadcasterSubscriptions.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def check_user_subscription( + self, + access_token: str, + broadcaster_id: int, + user_id: int, + *, + cache_time: int | None = None, + ): + req = await self.get( + '/subscriptions/user', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'user_id': user_id, + } + ), + ) + + match req.status_code: + case st.OK: + return s.UserSubscription.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_channel_teams( + self, + access_token: str, + broadcaster_id: int, + *, + cache_time: int | None = None, + ): + req = await self.get( + '/teams/channel', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.ChannelTeams.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_teams( + self, + access_token: str, + *, + name: str | None = None, + team_id: int | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/teams', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict({'name': name, 'id': team_id}), + ) + + match req.status_code: + case st.OK: + return s.Teams.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_users( + self, + access_token: str, + *, + user_id: int | list[int] | None = None, + login: str | list[str] | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/users', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict({'id': user_id, 'login': login}), + ) + + match req.status_code: + case st.OK: + return s.Users.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def update_user(self, access_token: str, *, description: str = ''): + req = await self.put( + '/users', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + } + ), + json={'description': description}, + ) + + match req.status_code: + case st.OK: + return s.Users.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_authorization_by_user( + self, + access_token: str, + user_id: int | list[int], + *, + cache_time: int | None = None, + ): + req = await self.get( + '/authorization/users', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params={'user_id': user_id}, + ) + + match req.status_code: + case st.OK: + return s.AuthorizationsByUser.model_validate(req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.INTERNAL_SERVER_ERROR + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_user_block_list( + self, + access_token: str, + broadcaster_id: int, + *, + first: int = 20, + after: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/users/blocks', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'first': first, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.UserBlockList.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def block_user( + self, + access_token: str, + target_user_id: int, + *, + source_context: Literal['chat', 'whisper'] | None = None, + reason: Literal['harassment', 'spam', 'other'] | None = None, + ): + req = await self.put( + '/users/blocks', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + } + ), + json=self.clean_dict( + { + 'target_user_id': target_user_id, + 'source_context': source_context, + 'reason': reason, + } + ), + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def unblock_user(self, access_token: str, target_user_id: int): + req = await self.delete( + '/users/blocks', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + } + ), + params={'target_user_id': target_user_id}, + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_user_extensions( + self, access_token: str, *, cache_time: int | None = None + ): + req = await self.get( + '/users/extensions/list', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + ) + + match req.status_code: + case st.OK: + return s.UserExtensions.model_validate(req.json()) + + case st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_user_active_extensions( + self, + access_token: str, + *, + user_id: int | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/users/extensions', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params={'user_id': user_id}, + ) + + match req.status_code: + case st.OK: + return s.UserActiveExtensions.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def update_user_extensions( + self, access_token: str, extensions: s.UserActiveExtensions + ): + req = await self.put( + '/users/extensions', + headers={ + 'Authorization': f'Bearer {access_token}', + }, + json=extensions.model_dump(), + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_videos( + self, + access_token: str, + video_id: int | list[int], + user_id: int, + game_id: int, + *, + language: str | None = None, + period: Literal['all', 'day', 'month', 'week'] = 'all', + sort: Literal['time', 'trending', 'views'] = 'time', + video_type: Literal['all', 'archive', 'highlight', 'upload'] = 'all', + first: int = 20, + after: str | None = None, + cache_time: int | None = None, + ): + req = await self.get( + '/videos', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': cache_time, + } + ), + params=self.clean_dict( + { + 'video_id': video_id, + 'user_id': user_id, + 'game_id': game_id, + 'language': language, + 'period': period, + 'sort': sort, + 'video_type': video_type, + 'first': first, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Videos.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def delete_videos( + self, access_token: str, video_id: int | list[int] + ): + req = await self.delete( + '/videos', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + } + ), + params={'id': video_id}, + ) + + match req.status_code: + case st.OK: + return s.DeleteVideos.model_validate(req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def send_whisper( + self, + access_token: str, + from_user_id: int, + to_user_id: int, + message: str, + ): + req = await self.post( + '/whispers', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + } + ), + params={'from_user_id': from_user_id, 'to_user_id': to_user_id}, + json={'message': message}, + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.NOT_FOUND + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError(req.status_code, req.json()['message']) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') diff --git a/src/twitchclient/auth.py b/src/twitchclient/auth.py new file mode 100644 index 0000000..c00d83f --- /dev/null +++ b/src/twitchclient/auth.py @@ -0,0 +1,126 @@ +from urllib.parse import urlencode + +from aiohttpx import status as st +from aiohttpx.client import AioHTTPXClient + +from . import schema as s +from . import scopes + + +class TwitchAuthClient(AioHTTPXClient): + def __init__( + self, + redis_url: str, + client_id: str, + client_secret: str, + redirect_uri: str, + ): + self.base_uri = 'https://id.twitch.tv/oauth2' + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + + super().__init__( + base_url=self.base_uri, + redis_url=redis_url, + key='twitch', + limit=700, + logger='Twitch Auth', + ) + + async def create_authorization_code_grant_flow_url( + self, + scope: list[scopes.Any], + *, + force_verify: bool = False, + state: str | None = None, + ): + url = 'https://id.twitch.tv/oauth2/authorize?' + query = self.clean_dict( + { + 'client_id': self.client_id, + 'redirect_uri': self.redirect_uri, + 'response_type': 'code', + 'scope': ' '.join(scope), + 'state': state, + 'force_verify': force_verify, + } + ) + + return url + urlencode(query) + + async def app_access_token(self): + req = await self.post( + '/token', + json={ + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'grant_type': 'client_credentials', + }, + ) + + match req.status_code: + case st.OK: + return s.AppAccessToken.model_validate(req.json()) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def user_access_token(self, code: str): + req = await self.post( + '/token', + json={ + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'redirect_uri': self.redirect_uri, + 'grant_type': 'authorization_code', + 'code': code, + }, + ) + + match req.status_code: + case st.OK: + return s.UserAccessToken.model_validate(req.json()) + + case st.BAD_REQUEST: + return None + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def validate_access_token(self, access_token: str): + req = await self.get( + '/validate', + headers={'Authorization': f'OAuth {access_token}'}, + ) + + match req.status_code: + case st.OK: + return s.AccessTokenValidation.model_validate(req.json()) + + case st.UNAUTHORIZED: + return None + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def refresh_access_token(self, refresh_token: str): + req = await self.post( + '/token', + json={ + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'grant_type': 'refresh_token', + 'refresh_token': refresh_token, + }, + ) + + match req.status_code: + case st.OK: + return s.UserAccessToken.model_validate(req.json()) + + case st.BAD_REQUEST: + return None + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') diff --git a/src/twitchclient/eventsub/__init__.py b/src/twitchclient/eventsub/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/twitchclient/eventsub/conditions.py b/src/twitchclient/eventsub/conditions.py new file mode 100644 index 0000000..4df29e0 --- /dev/null +++ b/src/twitchclient/eventsub/conditions.py @@ -0,0 +1,51 @@ +from pydantic import BaseModel + + +class User(BaseModel): + user_id: str + + +class Broadcaster(BaseModel): + broadcaster_user_id: str + + +class ToBroadcaster(BaseModel): + to_broadcaster_user_id: str + + +class FromBroadcaster(BaseModel): + from_broadcaster_user_id: str + + +class BroadcasterUser(Broadcaster, User): + pass + + +class BroadcasterModerator(Broadcaster): + moderator_user_id: str + + +class ClientID(BaseModel): + client_id: str + + +class Organization(BaseModel): + organization_id: int + category_id: int | None = None + campaign_id: int | None = None + + +class ExtensionClientID(BaseModel): + extension_client_id: str + + +Any = ( + Broadcaster + | ToBroadcaster + | FromBroadcaster + | BroadcasterUser + | BroadcasterModerator + | ClientID + | Organization + | ExtensionClientID +) diff --git a/src/twitchclient/eventsub/statuses.py b/src/twitchclient/eventsub/statuses.py new file mode 100644 index 0000000..2839ccf --- /dev/null +++ b/src/twitchclient/eventsub/statuses.py @@ -0,0 +1,43 @@ +from typing import Literal + +ENABLED = Literal['enabled'] +PENDING = Literal['webhook_callback_verification_pending'] +FAILED = Literal['webhook_callback_verification_failed'] +EXCEEDED = Literal['notification_failures_exceeded'] +REVOKED = Literal['authorization_revoked'] +MODERATOR_REMOVED = Literal['moderator_removed'] +USER_REMOVED = Literal['user_removed'] +CHAT_USER_BANNED = Literal['chat_user_banned'] +VERSION_REMOVED = Literal['version_removed'] +BETA_MAINTENANCE = Literal['beta_maintenance'] +WEBSOCKET_DISCONNECTED = Literal['websocket_disconnected'] +WEBSOCKET_FAILED_PING_PONG = Literal['websocket_failed_ping_pong'] +WEBSOCKET_RECEIVED_INBOUND_TRAFFIC = Literal[ + 'websocket_received_inbound_traffic' +] +WEBSOCKET_CONNECTION_UNUSED = Literal['websocket_connection_unused'] +WEBSOCKET_INTERNAL_ERROR = Literal['websocket_internal_error'] +WEBSOCKET_NETWORK_TIMEOUT = Literal['websocket_network_timeout'] +WEBSOCKET_NETWORK_ERROR = Literal['websocket_network_error'] +WEBSOCKET_FAILED_TO_RECONNECT = Literal['websocket_failed_to_reconnect'] + +Any = ( + ENABLED + | PENDING + | FAILED + | EXCEEDED + | REVOKED + | MODERATOR_REMOVED + | USER_REMOVED + | CHAT_USER_BANNED + | VERSION_REMOVED + | BETA_MAINTENANCE + | WEBSOCKET_DISCONNECTED + | WEBSOCKET_FAILED_PING_PONG + | WEBSOCKET_RECEIVED_INBOUND_TRAFFIC + | WEBSOCKET_CONNECTION_UNUSED + | WEBSOCKET_INTERNAL_ERROR + | WEBSOCKET_NETWORK_TIMEOUT + | WEBSOCKET_NETWORK_ERROR + | WEBSOCKET_FAILED_TO_RECONNECT +) diff --git a/src/twitchclient/eventsub/subscriptions.py b/src/twitchclient/eventsub/subscriptions.py new file mode 100644 index 0000000..ca8e803 --- /dev/null +++ b/src/twitchclient/eventsub/subscriptions.py @@ -0,0 +1,564 @@ +from datetime import datetime + +from pydantic import BaseModel + +from . import conditions as c +from . import statuses as s +from . import transports as t +from . import types as st + + +class BaseSubscription(BaseModel): + id: str + version: int + status: s.Any + cost: int + created_at: datetime + transport: t.Any + + default_version: int = 1 + + +class AutomodMessageHold(BaseSubscription): + type: st.AUTOMOD_MESSAGE_HOLD + condition: c.BroadcasterModerator + default_version: int = 2 + + +class AutomodMessageUpdate(BaseSubscription): + type: st.AUTOMOD_MESSAGE_UPDATE + condition: c.BroadcasterModerator + default_version: int = 2 + + +class AutomodSettingsUpdate(BaseSubscription): + type: st.AUTOMOD_SETTINGS_UPDATE + condition: c.BroadcasterModerator + default_version: int = 1 + + +class AutomodTermsUpdate(BaseSubscription): + type: st.AUTOMOD_TERMS_UPDATE + condition: c.BroadcasterModerator + default_version: int = 1 + + +class ChannelBitsUse(BaseSubscription): + type: st.CHANNEL_BITS_USE + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelUpdate(BaseSubscription): + type: st.CHANNEL_UPDATE + condition: c.Broadcaster + default_version: int = 2 + + +class ChannelFollow(BaseSubscription): + type: st.CHANNEL_FOLLOW + condition: c.BroadcasterModerator + default_version: int = 2 + + +class ChannelAdBreakBegin(BaseSubscription): + type: st.CHANNEL_AD_BREAK_BEGIN + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelChatClear(BaseSubscription): + type: st.CHANNEL_CHAT_CLEAR + condition: c.BroadcasterUser + default_version: int = 1 + + +class ChannelChatClearUserMessages(BaseSubscription): + type: st.CHANNEL_CHAT_CLEAR_USER_MESSAGES + condition: c.BroadcasterUser + default_version: int = 1 + + +class ChannelChatMessage(BaseSubscription): + type: st.CHANNEL_CHAT_MESSAGE + condition: c.BroadcasterUser + default_version: int = 1 + + +class ChannelChatMessageDelete(BaseSubscription): + type: st.CHANNEL_CHAT_MESSAGE_DELETE + condition: c.BroadcasterUser + default_version: int = 1 + + +class ChannelChatNotification(BaseSubscription): + type: st.CHANNEL_CHAT_NOTIFICATION + condition: c.BroadcasterUser + default_version: int = 1 + + +class ChannelChatSettingsUpdate(BaseSubscription): + type: st.CHANNEL_CHAT_SETTINGS_UPDATE + condition: c.BroadcasterUser + default_version: int = 1 + + +class ChannelChatUserMessageHold(BaseSubscription): + type: st.CHANNEL_CHAT_USER_MESSAGE_HOLD + condition: c.BroadcasterUser + default_version: int = 1 + + +class ChannelChatUserMessageUpdate(BaseSubscription): + type: st.CHANNEL_CHAT_USER_MESSAGE_UPDATE + condition: c.BroadcasterUser + default_version: int = 1 + + +class ChannelSharedChatSessionBegin(BaseSubscription): + type: st.CHANNEL_SHARED_CHAT_SESSION_BEGIN + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelSharedChatSessionUpdate(BaseSubscription): + type: st.CHANNEL_SHARED_CHAT_SESSION_UPDATE + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelSharedChatSessionEnd(BaseSubscription): + type: st.CHANNEL_SHARED_CHAT_SESSION_END + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelSubscribe(BaseSubscription): + type: st.CHANNEL_SUBSCRIBE + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelSubscriptionEnd(BaseSubscription): + type: st.CHANNEL_SUBSCRIPTION_END + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelSubscriptionGift(BaseSubscription): + type: st.CHANNEL_SUBSCRIPTION_GIFT + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelSubscriptionMessage(BaseSubscription): + type: st.CHANNEL_SUBSCRIPTION_MESSAGE + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelCheer(BaseSubscription): + type: st.CHANNEL_CHEER + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelRaid(BaseSubscription): + type: st.CHANNEL_RAID + condition: c.ToBroadcaster + default_version: int = 1 + + +class ChannelBan(BaseSubscription): + type: st.CHANNEL_BAN + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelUnban(BaseSubscription): + type: st.CHANNEL_UNBAN + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelUnbanRequestCreate(BaseSubscription): + type: st.CHANNEL_UNBAN_REQUEST_CREATE + condition: c.BroadcasterModerator + default_version: int = 1 + + +class ChannelUnbanRequestResolve(BaseSubscription): + type: st.CHANNEL_UNBAN_REQUEST_RESOLVE + condition: c.BroadcasterModerator + default_version: int = 1 + + +class ChannelModerate(BaseSubscription): + type: st.CHANNEL_MODERATE + condition: c.BroadcasterModerator + default_version: int = 2 + + +class ChannelModeratorAdd(BaseSubscription): + type: st.CHANNEL_MODERATOR_ADD + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelModeratorRemove(BaseSubscription): + type: st.CHANNEL_MODERATOR_REMOVE + condition: c.Broadcaster + default_version: int = 1 + + +# TODO: Complete when subscription version is released +# class ChannelGuestStarSessionBegin(BaseSubscription): +# type: st.CHANNEL_GUEST_STAR_SESSION_BEGIN +# condition: c.Broadcaster +# default_version: int = 1 + +# TODO: Complete when subscription version is released +# class ChannelGuestStarSessionEnd(BaseSubscription): +# type: st.CHANNEL_GUEST_STAR_SESSION_END +# condition: c.Broadcaster +# default_version: int = 1 + +# TODO: Complete when subscription version is released +# class ChannelGuestStarGuestUpdate(BaseSubscription): +# type: st.CHANNEL_GUEST_STAR_GUEST_UPDATE +# condition: c.Broadcaster +# default_version: int = 1 + +# TODO: Complete when subscription version is released +# class ChannelGuestStarSettingsUpdate(BaseSubscription): +# type: st.CHANNEL_GUEST_STAR_SETTINGS_UPDATE +# condition: c.Broadcaster +# default_version: int = 1 + + +class ChannelPointsAutomaticRewardRedemptionAdd(BaseSubscription): + type: st.CHANNEL_POINTS_AUTOMATIC_REWARD_REDEMPTION_ADD + condition: c.Broadcaster + default_version: int = 2 + + +class ChannelPointsCustomRewardAdd(BaseSubscription): + type: st.CHANNEL_POINTS_CUSTOM_REWARD_ADD + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelPointsCustomRewardUpdate(BaseSubscription): + type: st.CHANNEL_POINTS_CUSTOM_REWARD_UPDATE + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelPointsCustomRewardRemove(BaseSubscription): + type: st.CHANNEL_POINTS_CUSTOM_REWARD_REMOVE + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelPointsCustomRewardRedemptionAdd(BaseSubscription): + type: st.CHANNEL_POINTS_CUSTOM_REWARD_REDEMPTION_ADD + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelPointsCustomRewardRedemptionUpdate(BaseSubscription): + type: st.CHANNEL_POINTS_CUSTOM_REWARD_REDEMPTION_UPDATE + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelPollBegin(BaseSubscription): + type: st.CHANNEL_POLL_BEGIN + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelPollProgress(BaseSubscription): + type: st.CHANNEL_POLL_PROGRESS + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelPollEnd(BaseSubscription): + type: st.CHANNEL_POLL_END + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelPredictionBegin(BaseSubscription): + type: st.CHANNEL_PREDICTION_BEGIN + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelPredictionProgress(BaseSubscription): + type: st.CHANNEL_PREDICTION_PROGRESS + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelPredictionLock(BaseSubscription): + type: st.CHANNEL_PREDICTION_LOCK + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelPredictionEnd(BaseSubscription): + type: st.CHANNEL_PREDICTION_END + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelSuspiciousUserMessage(BaseSubscription): + type: st.CHANNEL_SUSPICIOUS_USER_MESSAGE + condition: c.BroadcasterModerator + default_version: int = 1 + + +class ChannelSuspiciousUserUpdate(BaseSubscription): + type: st.CHANNEL_SUSPICIOUS_USER_UPDATE + condition: c.BroadcasterModerator + default_version: int = 1 + + +class ChannelVIPAdd(BaseSubscription): + type: st.CHANNEL_VIP_ADD + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelVIPRemove(BaseSubscription): + type: st.CHANNEL_VIP_REMOVE + condition: c.Broadcaster + default_version: int = 1 + + +class ChannelWarningAcknowledgement(BaseSubscription): + type: st.CHANNEL_WARNING_ACKNOWLEDGEMENT + condition: c.BroadcasterModerator + default_version: int = 1 + + +class ChannelWarningSend(BaseSubscription): + type: st.CHANNEL_WARNING_SEND + condition: c.BroadcasterModerator + default_version: int = 1 + + +class CharityDonation(BaseSubscription): + type: st.CHARITY_DONATION + condition: c.Broadcaster + default_version: int = 1 + + +class CharityCampaignStart(BaseSubscription): + type: st.CHARITY_CAMPAIGN_START + condition: c.Broadcaster + default_version: int = 1 + + +class CharityCampaignProgress(BaseSubscription): + type: st.CHARITY_CAMPAIGN_PROGRESS + condition: c.Broadcaster + default_version: int = 1 + + +class CharityCampaignStop(BaseSubscription): + type: st.CHARITY_CAMPAIGN_STOP + condition: c.Broadcaster + default_version: int = 1 + + +class ConduitShardDisabled(BaseSubscription): + type: st.CONDUIT_SHARD_DISABLED + condition: c.ClientID + default_version: int = 1 + + +class DropEntitlementGrant(BaseSubscription): + type: st.DROP_ENTITLEMENT_GRANT + condition: c.Organization + default_version: int = 1 + + +class ExtensionBitsTransactionCreate(BaseSubscription): + type: st.EXTENSION_BITS_TRANSACTION_CREATE + condition: c.ExtensionClientID + default_version: int = 1 + + +class GoalBegin(BaseSubscription): + type: st.GOAL_BEGIN + condition: c.Broadcaster + default_version: int = 1 + + +class GoalProgress(BaseSubscription): + type: st.GOAL_PROGRESS + condition: c.Broadcaster + default_version: int = 1 + + +class GoalEnd(BaseSubscription): + type: st.GOAL_END + condition: c.Broadcaster + default_version: int = 1 + + +class HypeTrainBegin(BaseSubscription): + type: st.HYPE_TRAIN_BEGIN + condition: c.Broadcaster + default_version: int = 2 + + +class HypeTrainProgress(BaseSubscription): + type: st.HYPE_TRAIN_PROGRESS + condition: c.Broadcaster + default_version: int = 2 + + +class HypeTrainEnd(BaseSubscription): + type: st.HYPE_TRAIN_END + condition: c.Broadcaster + default_version: int = 2 + + +class ShieldModeBegin(BaseSubscription): + type: st.SHIELD_MODE_BEGIN + condition: c.BroadcasterModerator + default_version: int = 1 + + +class ShieldModeEnd(BaseSubscription): + type: st.SHIELD_MODE_END + condition: c.BroadcasterModerator + default_version: int = 1 + + +class ShoutoutCreate(BaseSubscription): + type: st.SHOUTOUT_CREATE + condition: c.BroadcasterModerator + default_version: int = 1 + + +class ShoutoutReceived(BaseSubscription): + type: st.SHOUTOUT_RECEIVED + condition: c.BroadcasterModerator + default_version: int = 1 + + +class StreamOnline(BaseSubscription): + type: st.STREAM_ONLINE + condition: c.Broadcaster + default_version: int = 1 + + +class StreamOffline(BaseSubscription): + type: st.STREAM_OFFLINE + condition: c.Broadcaster + default_version: int = 1 + + +class UserAuthorizationGrant(BaseSubscription): + type: st.USER_AUTHORIZATION_GRANT + condition: c.ClientID + default_version: int = 1 + + +class UserAuthorizationRevoke(BaseSubscription): + type: st.USER_AUTHORIZATION_REVOKE + condition: c.ClientID + default_version: int = 1 + + +class UserUpdate(BaseSubscription): + type: st.USER_UPDATE + condition: c.User + default_version: int = 1 + + +class WhisperReceived(BaseSubscription): + type: st.WHISPER_RECEIVED + condition: c.User + default_version: int = 1 + + +Any = ( + AutomodMessageHold + | AutomodMessageUpdate + | AutomodSettingsUpdate + | AutomodTermsUpdate + | ChannelBitsUse + | ChannelUpdate + | ChannelFollow + | ChannelAdBreakBegin + | ChannelChatClear + | ChannelChatClearUserMessages + | ChannelChatMessage + | ChannelChatMessageDelete + | ChannelChatNotification + | ChannelChatSettingsUpdate + | ChannelChatUserMessageHold + | ChannelChatUserMessageUpdate + | ChannelSharedChatSessionBegin + | ChannelSharedChatSessionUpdate + | ChannelSharedChatSessionEnd + | ChannelSubscribe + | ChannelSubscriptionEnd + | ChannelSubscriptionGift + | ChannelSubscriptionMessage + | ChannelCheer + | ChannelRaid + | ChannelBan + | ChannelUnban + | ChannelUnbanRequestCreate + | ChannelUnbanRequestResolve + | ChannelModerate + | ChannelModeratorAdd + | ChannelModeratorRemove + | ChannelPointsAutomaticRewardRedemptionAdd + | ChannelPointsCustomRewardAdd + | ChannelPointsCustomRewardUpdate + | ChannelPointsCustomRewardRemove + | ChannelPointsCustomRewardRedemptionAdd + | ChannelPointsCustomRewardRedemptionUpdate + | ChannelPollBegin + | ChannelPollProgress + | ChannelPollEnd + | ChannelPredictionBegin + | ChannelPredictionProgress + | ChannelPredictionLock + | ChannelPredictionEnd + | ChannelSuspiciousUserMessage + | ChannelSuspiciousUserUpdate + | ChannelVIPAdd + | ChannelVIPRemove + | ChannelWarningAcknowledgement + | ChannelWarningSend + | CharityDonation + | CharityCampaignStart + | CharityCampaignProgress + | CharityCampaignStop + | ConduitShardDisabled + | DropEntitlementGrant + | ExtensionBitsTransactionCreate + | GoalBegin + | GoalProgress + | GoalEnd + | HypeTrainBegin + | HypeTrainProgress + | HypeTrainEnd + | ShoutoutCreate + | ShoutoutReceived + | StreamOnline + | StreamOffline + | UserAuthorizationGrant + | UserAuthorizationRevoke + | UserUpdate + | WhisperReceived +) diff --git a/src/twitchclient/eventsub/transports.py b/src/twitchclient/eventsub/transports.py new file mode 100644 index 0000000..3aea9e1 --- /dev/null +++ b/src/twitchclient/eventsub/transports.py @@ -0,0 +1,22 @@ +from datetime import datetime +from typing import Literal + +from pydantic import BaseModel + + +class BaseWebhookTransport(BaseModel): + method: Literal['webhook'] + callback: str + + +class BaseWebsocketTransport(BaseModel): + method: Literal['websocket'] + session_id: str + connected_at: datetime + + +class SubscribeWebhookTransport(BaseWebhookTransport): + secret: str + + +Any = BaseWebhookTransport | BaseWebsocketTransport diff --git a/src/twitchclient/eventsub/types.py b/src/twitchclient/eventsub/types.py new file mode 100644 index 0000000..d71899c --- /dev/null +++ b/src/twitchclient/eventsub/types.py @@ -0,0 +1,201 @@ +from typing import Literal + +# Automod types +AUTOMOD_MESSAGE_HOLD = Literal['automod.message.hold'] +AUTOMOD_MESSAGE_UPDATE = Literal['automod.message.update'] +AUTOMOD_SETTINGS_UPDATE = Literal['automod.settings.update'] +AUTOMOD_TERMS_UPDATE = Literal['automod.terms.update'] + +# Channel types +CHANNEL_BITS_USE = Literal['channel.bits.use'] +CHANNEL_UPDATE = Literal['channel.update'] +CHANNEL_FOLLOW = Literal['channel.follow'] +CHANNEL_AD_BREAK_BEGIN = Literal['channel.ad_break.begin'] +CHANNEL_CHAT_CLEAR = Literal['channel.chat.clear'] +CHANNEL_CHAT_CLEAR_USER_MESSAGES = Literal['channel.chat.clear_user_messages'] +CHANNEL_CHAT_MESSAGE = Literal['channel.chat.message'] +CHANNEL_CHAT_MESSAGE_DELETE = Literal['channel.chat.message_delete'] +CHANNEL_CHAT_NOTIFICATION = Literal['channel.chat.notification'] +CHANNEL_CHAT_SETTINGS_UPDATE = Literal['channel.chat_settings.update'] +CHANNEL_CHAT_USER_MESSAGE_HOLD = Literal['channel.chat.user_message_hold'] +CHANNEL_CHAT_USER_MESSAGE_UPDATE = Literal['channel.chat.user_message_update'] +CHANNEL_SHARED_CHAT_SESSION_BEGIN = Literal['channel.shared_chat.begin'] +CHANNEL_SHARED_CHAT_SESSION_UPDATE = Literal['channel.shared_chat.update'] +CHANNEL_SHARED_CHAT_SESSION_END = Literal['channel.shared_chat.end'] +CHANNEL_SUBSCRIBE = Literal['channel.subscribe'] +CHANNEL_SUBSCRIPTION_END = Literal['channel.subscription.end'] +CHANNEL_SUBSCRIPTION_GIFT = Literal['channel.subscription.gift'] +CHANNEL_SUBSCRIPTION_MESSAGE = Literal['channel.subscription.message'] +CHANNEL_CHEER = Literal['channel.cheer'] +CHANNEL_RAID = Literal['channel.raid'] +CHANNEL_BAN = Literal['channel.ban'] +CHANNEL_UNBAN = Literal['channel.unban'] +CHANNEL_UNBAN_REQUEST_CREATE = Literal['channel.unban_request.create'] +CHANNEL_UNBAN_REQUEST_RESOLVE = Literal['channel.unban_request.resolve'] +CHANNEL_MODERATE = Literal['channel.moderate'] +CHANNEL_MODERATOR_ADD = Literal['channel.moderator.add'] +CHANNEL_MODERATOR_REMOVE = Literal['channel.moderator.remove'] +CHANNEL_GUEST_STAR_SESSION_BEGIN = Literal['channel.guest_star_session.begin'] +CHANNEL_GUEST_STAR_SESSION_END = Literal['channel.guest_star_session.end'] +CHANNEL_GUEST_STAR_GUEST_UPDATE = Literal['channel.guest_star_guest.update'] +CHANNEL_GUEST_STAR_SETTINGS_UPDATE = Literal[ + 'channel.guest_star_settings.update' +] +CHANNEL_POINTS_AUTOMATIC_REWARD_REDEMPTION_ADD = Literal[ + 'channel.channel_points_automatic_reward_redemption.add' +] +CHANNEL_POINTS_CUSTOM_REWARD_ADD = Literal[ + 'channel.channel_points_custom_reward.add' +] +CHANNEL_POINTS_CUSTOM_REWARD_UPDATE = Literal[ + 'channel.channel_points_custom_reward.update' +] +CHANNEL_POINTS_CUSTOM_REWARD_REMOVE = Literal[ + 'channel.channel_points_custom_reward.remove' +] +CHANNEL_POINTS_CUSTOM_REWARD_REDEMPTION_ADD = Literal[ + 'channel.channel_points_custom_reward_redemption.add' +] +CHANNEL_POINTS_CUSTOM_REWARD_REDEMPTION_UPDATE = Literal[ + 'channel.channel_points_custom_reward_redemption.update' +] +CHANNEL_POLL_BEGIN = Literal['channel.poll.begin'] +CHANNEL_POLL_PROGRESS = Literal['channel.poll.progress'] +CHANNEL_POLL_END = Literal['channel.poll.end'] +CHANNEL_PREDICTION_BEGIN = Literal['channel.prediction.begin'] +CHANNEL_PREDICTION_PROGRESS = Literal['channel.prediction.progress'] +CHANNEL_PREDICTION_LOCK = Literal['channel.prediction.lock'] +CHANNEL_PREDICTION_END = Literal['channel.prediction.end'] +CHANNEL_SUSPICIOUS_USER_MESSAGE = Literal['channel.suspicious_user.message'] +CHANNEL_SUSPICIOUS_USER_UPDATE = Literal['channel.suspicious_user.update'] +CHANNEL_VIP_ADD = Literal['channel.vip.add'] +CHANNEL_VIP_REMOVE = Literal['channel.vip.remove'] +CHANNEL_WARNING_ACKNOWLEDGEMENT = Literal['channel.warning.acknowledge'] +CHANNEL_WARNING_SEND = Literal['channel.warning.send'] + +# Charity types +CHARITY_DONATION = Literal['channel.charity_campaign.donate'] +CHARITY_CAMPAIGN_START = Literal['channel.charity_campaign.start'] +CHARITY_CAMPAIGN_PROGRESS = Literal['channel.charity_campaign.progress'] +CHARITY_CAMPAIGN_STOP = Literal['channel.charity_campaign.stop'] + +# Conduit types +CONDUIT_SHARD_DISABLED = Literal['conduit.shard.disabled'] + +# Drop types +DROP_ENTITLEMENT_GRANT = Literal['drop.entitlement.grant'] + +# Extension types +EXTENSION_BITS_TRANSACTION_CREATE = Literal[ + 'extension.bits_transaction.create' +] + +# Goal types +GOAL_BEGIN = Literal['channel.goal.begin'] +GOAL_PROGRESS = Literal['channel.goal.progress'] +GOAL_END = Literal['channel.goal.end'] + +# Hype Train types +HYPE_TRAIN_BEGIN = Literal['channel.hype_train.begin'] +HYPE_TRAIN_PROGRESS = Literal['channel.hype_train.progress'] +HYPE_TRAIN_END = Literal['channel.hype_train.end'] + +# Shield Mode types +SHIELD_MODE_BEGIN = Literal['channel.shield_mode.begin'] +SHIELD_MODE_END = Literal['channel.shield_mode.end'] + +# Shoutout types +SHOUTOUT_CREATE = Literal['channel.shoutout.create'] +SHOUTOUT_RECEIVED = Literal['channel.shoutout.receive'] + +# Stream types +STREAM_ONLINE = Literal['stream.online'] +STREAM_OFFLINE = Literal['stream.offline'] + +# User types +USER_AUTHORIZATION_GRANT = Literal['user.authorization.grant'] +USER_AUTHORIZATION_REVOKE = Literal['user.authorization.revoke'] +USER_UPDATE = Literal['user.update'] +WHISPER_RECEIVED = Literal['user.whisper.message'] + +# Union type for all events +Any = ( + AUTOMOD_MESSAGE_HOLD + | AUTOMOD_MESSAGE_UPDATE + | AUTOMOD_SETTINGS_UPDATE + | AUTOMOD_TERMS_UPDATE + | CHANNEL_BITS_USE + | CHANNEL_UPDATE + | CHANNEL_FOLLOW + | CHANNEL_AD_BREAK_BEGIN + | CHANNEL_CHAT_CLEAR + | CHANNEL_CHAT_CLEAR_USER_MESSAGES + | CHANNEL_CHAT_MESSAGE + | CHANNEL_CHAT_MESSAGE_DELETE + | CHANNEL_CHAT_NOTIFICATION + | CHANNEL_CHAT_SETTINGS_UPDATE + | CHANNEL_CHAT_USER_MESSAGE_HOLD + | CHANNEL_CHAT_USER_MESSAGE_UPDATE + | CHANNEL_SHARED_CHAT_SESSION_BEGIN + | CHANNEL_SHARED_CHAT_SESSION_UPDATE + | CHANNEL_SHARED_CHAT_SESSION_END + | CHANNEL_SUBSCRIBE + | CHANNEL_SUBSCRIPTION_END + | CHANNEL_SUBSCRIPTION_GIFT + | CHANNEL_SUBSCRIPTION_MESSAGE + | CHANNEL_CHEER + | CHANNEL_RAID + | CHANNEL_BAN + | CHANNEL_UNBAN + | CHANNEL_UNBAN_REQUEST_CREATE + | CHANNEL_UNBAN_REQUEST_RESOLVE + | CHANNEL_MODERATE + | CHANNEL_MODERATOR_ADD + | CHANNEL_MODERATOR_REMOVE + | CHANNEL_GUEST_STAR_SESSION_BEGIN + | CHANNEL_GUEST_STAR_SESSION_END + | CHANNEL_GUEST_STAR_GUEST_UPDATE + | CHANNEL_GUEST_STAR_SETTINGS_UPDATE + | CHANNEL_POINTS_AUTOMATIC_REWARD_REDEMPTION_ADD + | CHANNEL_POINTS_CUSTOM_REWARD_ADD + | CHANNEL_POINTS_CUSTOM_REWARD_UPDATE + | CHANNEL_POINTS_CUSTOM_REWARD_REMOVE + | CHANNEL_POINTS_CUSTOM_REWARD_REDEMPTION_ADD + | CHANNEL_POINTS_CUSTOM_REWARD_REDEMPTION_UPDATE + | CHANNEL_POLL_BEGIN + | CHANNEL_POLL_PROGRESS + | CHANNEL_POLL_END + | CHANNEL_PREDICTION_BEGIN + | CHANNEL_PREDICTION_PROGRESS + | CHANNEL_PREDICTION_LOCK + | CHANNEL_PREDICTION_END + | CHANNEL_SUSPICIOUS_USER_MESSAGE + | CHANNEL_SUSPICIOUS_USER_UPDATE + | CHANNEL_VIP_ADD + | CHANNEL_VIP_REMOVE + | CHANNEL_WARNING_ACKNOWLEDGEMENT + | CHANNEL_WARNING_SEND + | CHARITY_DONATION + | CHARITY_CAMPAIGN_START + | CHARITY_CAMPAIGN_PROGRESS + | CHARITY_CAMPAIGN_STOP + | CONDUIT_SHARD_DISABLED + | DROP_ENTITLEMENT_GRANT + | EXTENSION_BITS_TRANSACTION_CREATE + | GOAL_BEGIN + | GOAL_PROGRESS + | GOAL_END + | HYPE_TRAIN_BEGIN + | HYPE_TRAIN_PROGRESS + | HYPE_TRAIN_END + | SHIELD_MODE_BEGIN + | SHIELD_MODE_END + | SHOUTOUT_CREATE + | SHOUTOUT_RECEIVED + | STREAM_ONLINE + | STREAM_OFFLINE + | USER_AUTHORIZATION_GRANT + | USER_AUTHORIZATION_REVOKE + | USER_UPDATE + | WHISPER_RECEIVED +) diff --git a/src/twitchclient/schema.py b/src/twitchclient/schema.py index bdde561..3bca79b 100644 --- a/src/twitchclient/schema.py +++ b/src/twitchclient/schema.py @@ -3,6 +3,13 @@ from typing import Any, Literal, TypedDict from pydantic import BaseModel, ConfigDict, Field +from . import scopes +from .eventsub import subscriptions as sub + + +class BaseSchema(BaseModel): + model_config = ConfigDict(extra='forbid') + class Error(Exception): status_code: int @@ -14,29 +21,51 @@ class Error(Exception): super().__init__(f'{status_code}: {error}') -class Pagination(BaseModel): - model_config = ConfigDict(extra='forbid') +class ClientError(Error): + pass + +class InternalError(Error): + pass + + +class Pagination(BaseSchema): cursor: str -class StartCommercialData(BaseModel): - model_config = ConfigDict(extra='forbid') +class AppAccessToken(BaseSchema): + access_token: str + expires_in: int + token_type: Literal['bearer'] + +class UserAccessToken(BaseSchema): + access_token: str + refresh_token: str + scope: list[scopes.Any] + expires_in: int + token_type: Literal['bearer'] + + +class AccessTokenValidation(BaseSchema): + client_id: str + login: str + scopes: list[scopes.Any] + user_id: int + expires_in: int + + +class StartCommercialData(BaseSchema): length: int message: str retry_after: int -class StartCommercial(BaseModel): - model_config = ConfigDict(extra='forbid') - +class StartCommercial(BaseSchema): data: list[StartCommercialData] -class AdScheduleData(BaseModel): - model_config = ConfigDict(extra='forbid') - +class AdScheduleData(BaseSchema): next_ad_at: datetime | None last_ad_at: datetime | None duration: int @@ -45,68 +74,50 @@ class AdScheduleData(BaseModel): snooze_refresh_at: datetime -class AdSchedule(BaseModel): - model_config = ConfigDict(extra='forbid') - +class AdSchedule(BaseSchema): data: list[AdScheduleData] -class SnoozeNextAdData(BaseModel): - model_config = ConfigDict(extra='forbid') - +class SnoozeNextAdData(BaseSchema): snooze_count: int snooze_refresh_at: datetime next_ad_at: datetime -class SnoozeNextAd(BaseModel): - model_config = ConfigDict(extra='forbid') - +class SnoozeNextAd(BaseSchema): data: list[SnoozeNextAdData] -class DateRange(BaseModel): - model_config = ConfigDict(extra='forbid') - +class DateRange(BaseSchema): started_at: datetime ended_at: datetime -class ExtensionAnalyticsData(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ExtensionAnalyticsData(BaseSchema): extension_id: str URL: str type: str date_range: DateRange -class ExtensionAnalytics(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ExtensionAnalytics(BaseSchema): data: list[ExtensionAnalyticsData] pagination: Pagination | dict[Any, Any] | None = None -class GameAnalyticsData(BaseModel): - model_config = ConfigDict(extra='forbid') - +class GameAnalyticsData(BaseSchema): game_id: int URL: str type: str date_range: DateRange -class GameAnalytics(BaseModel): - model_config = ConfigDict(extra='forbid') - +class GameAnalytics(BaseSchema): data: list[GameAnalyticsData] pagination: Pagination | dict[Any, Any] | None = None -class BitsLeaderboardData(BaseModel): - model_config = ConfigDict(extra='forbid') - +class BitsLeaderboardData(BaseSchema): user_id: int user_login: str user_name: str @@ -114,15 +125,13 @@ class BitsLeaderboardData(BaseModel): score: int -class BitsLeaderboard(BaseModel): - model_config = ConfigDict(extra='forbid') - +class BitsLeaderboard(BaseSchema): data: list[BitsLeaderboardData] date_range: DateRange total: int -class CheermotesImageAnimated(BaseModel): +class CheermotesImageAnimated(BaseSchema): field_1: str = Field(..., alias='1') field_1_5: str = Field(..., alias='1.5') field_2: str = Field(..., alias='2') @@ -130,7 +139,7 @@ class CheermotesImageAnimated(BaseModel): field_4: str = Field(..., alias='4') -class CheermotesImageStatic(BaseModel): +class CheermotesImageStatic(BaseSchema): field_1: str = Field(..., alias='1') field_1_5: str = Field(..., alias='1.5') field_2: str = Field(..., alias='2') @@ -138,21 +147,17 @@ class CheermotesImageStatic(BaseModel): field_4: str = Field(..., alias='4') -class CheermotesImage(BaseModel): +class CheermotesImage(BaseSchema): animated: CheermotesImageAnimated static: CheermotesImageStatic -class CheermotesImages(BaseModel): - model_config = ConfigDict(extra='forbid') - +class CheermotesImages(BaseSchema): light: CheermotesImage dark: CheermotesImage -class CheermotesTier(BaseModel): - model_config = ConfigDict(extra='forbid') - +class CheermotesTier(BaseSchema): min_bits: int id: Literal['1', '100', '500', '1000', '5000', '10000', '100000'] color: str @@ -161,9 +166,7 @@ class CheermotesTier(BaseModel): images: CheermotesImages -class CheermotesData(BaseModel): - model_config = ConfigDict(extra='forbid') - +class CheermotesData(BaseSchema): prefix: str tiers: list[CheermotesTier] type: Literal[ @@ -178,22 +181,16 @@ class CheermotesData(BaseModel): is_charitable: bool -class Cheermotes(BaseModel): - model_config = ConfigDict(extra='forbid') - +class Cheermotes(BaseSchema): data: list[CheermotesData] -class ExtensionProductCost(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ExtensionProductCost(BaseSchema): amount: int type: Literal['bits'] -class ExtensionProductData(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ExtensionProductData(BaseSchema): domain: str sku: str cost: ExtensionProductCost @@ -203,9 +200,7 @@ class ExtensionProductData(BaseModel): broadcast: bool -class ExtensionTransactionsData(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ExtensionTransactionsData(BaseSchema): id: str timestamp: datetime broadcaster_id: int @@ -218,9 +213,7 @@ class ExtensionTransactionsData(BaseModel): product_data: ExtensionProductData -class ExtensionTransactions(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ExtensionTransactions(BaseSchema): data: list[ExtensionTransactionsData] pagination: Pagination | dict[Any, Any] | None = None @@ -237,9 +230,7 @@ class ContentClassificationLabel(TypedDict): is_enabled: bool -class ChannelInformation(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ChannelInformation(BaseSchema): broadcaster_id: int broadcaster_login: str broadcaster_name: str @@ -253,92 +244,68 @@ class ChannelInformation(BaseModel): is_branded_content: bool -class ChannelsInformation(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ChannelsInformation(BaseSchema): data: list[ChannelInformation] -class ChannelEditor(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ChannelEditor(BaseSchema): user_id: int user_name: str created_at: datetime -class ChannelEditors(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ChannelEditors(BaseSchema): data: list[ChannelEditor] -class FollowedChannel(BaseModel): - model_config = ConfigDict(extra='forbid') - +class FollowedChannel(BaseSchema): broadcaster_id: int broadcaster_login: str broadcaster_name: str followed_at: datetime -class FollowedChannels(BaseModel): - model_config = ConfigDict(extra='forbid') - +class FollowedChannels(BaseSchema): data: list[FollowedChannel] pagination: Pagination | dict[Any, Any] | None = None total: int -class ChannelFollower(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ChannelFollower(BaseSchema): user_id: int user_login: str user_name: str followed_at: datetime -class ChannelFollowers(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ChannelFollowers(BaseSchema): data: list[ChannelFollower] pagination: Pagination | dict[Any, Any] | None = None total: int -class CustomRewardImage(BaseModel): - model_config = ConfigDict(extra='forbid') - +class CustomRewardImage(BaseSchema): url_1x: str url_2x: str url_4x: str -class MaxPerStreamSetting(BaseModel): - model_config = ConfigDict(extra='forbid') - +class MaxPerStreamSetting(BaseSchema): is_enabled: bool max_per_stream: int -class MaxPerUserPerStreamSetting(BaseModel): - model_config = ConfigDict(extra='forbid') - +class MaxPerUserPerStreamSetting(BaseSchema): is_enabled: bool max_per_user_per_stream: int -class GlobalCooldownSetting(BaseModel): - model_config = ConfigDict(extra='forbid') - +class GlobalCooldownSetting(BaseSchema): is_enabled: bool global_cooldown_seconds: int -class CustomReward(BaseModel): - model_config = ConfigDict(extra='forbid') - +class CustomReward(BaseSchema): broadcaster_id: int broadcaster_login: str broadcaster_name: str @@ -361,24 +328,18 @@ class CustomReward(BaseModel): global_cooldown_setting: GlobalCooldownSetting -class CustomRewards(BaseModel): - model_config = ConfigDict(extra='forbid') - +class CustomRewards(BaseSchema): data: list[CustomReward] -class CustomRewardRedemptionReward(BaseModel): - model_config = ConfigDict(extra='forbid') - +class CustomRewardRedemptionReward(BaseSchema): id: str title: str prompt: str cost: int -class CustomRewardRedemption(BaseModel): - model_config = ConfigDict(extra='forbid') - +class CustomRewardRedemption(BaseSchema): id: int broadcaster_id: int broadcaster_login: str @@ -392,31 +353,23 @@ class CustomRewardRedemption(BaseModel): reward: CustomRewardRedemptionReward -class CustomRewardRedemptions(BaseModel): - model_config = ConfigDict(extra='forbid') - +class CustomRewardRedemptions(BaseSchema): data: list[CustomRewardRedemption] -class CharityCampaignCurrentAmount(BaseModel): - model_config = ConfigDict(extra='forbid') - +class CharityCampaignCurrentAmount(BaseSchema): amount: int decimal_places: int currency: str -class CharityCampaignTargetAmount(BaseModel): - model_config = ConfigDict(extra='forbid') - +class CharityCampaignTargetAmount(BaseSchema): value: int decimal_places: int currency: str -class CharityCampaignData(BaseModel): - model_config = ConfigDict(extra='forbid') - +class CharityCampaignData(BaseSchema): id: str broadcaster_id: int broadcaster_login: str @@ -429,23 +382,17 @@ class CharityCampaignData(BaseModel): target_amount: CharityCampaignTargetAmount -class CharityCampaign(BaseModel): - model_config = ConfigDict(extra='forbid') - +class CharityCampaign(BaseSchema): data: list[CharityCampaignData] -class CharityDonationAmount(BaseModel): - model_config = ConfigDict(extra='forbid') - +class CharityDonationAmount(BaseSchema): value: int decimal_places: int currency: str -class CharityDonation(BaseModel): - model_config = ConfigDict(extra='forbid') - +class CharityDonation(BaseSchema): id: str campaign_id: str user_id: int @@ -454,40 +401,30 @@ class CharityDonation(BaseModel): amount: CharityDonationAmount -class CharityDonations(BaseModel): - model_config = ConfigDict(extra='forbid') - +class CharityDonations(BaseSchema): data: list[CharityDonation] pagination: Pagination | dict[Any, Any] | None = None -class ChattersData(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ChattersData(BaseSchema): user_id: int user_login: str user_name: str -class Chatters(BaseModel): - model_config = ConfigDict(extra='forbid') - +class Chatters(BaseSchema): data: list[ChattersData] pagination: Pagination | dict[Any, Any] | None = None total: int -class ChannelEmoteImages(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ChannelEmoteImages(BaseSchema): url_1x: str url_2x: str url_4x: str -class ChannelEmote(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ChannelEmote(BaseSchema): id: int name: str images: ChannelEmoteImages @@ -506,16 +443,14 @@ class ChannelEmote(BaseModel): 'globals', 'owl2019', 'twofactor', - 'limitedtime' + 'limitedtime', ] format: list[Literal['animated', 'static']] scale: list[Literal['1.0', '2.0', '3.0']] theme_mode: list[Literal['dark', 'light']] -class ChannelEmotes(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ChannelEmotes(BaseSchema): data: list[ChannelEmote] template: str @@ -528,9 +463,7 @@ class EmoteSets(ChannelEmotes): pass -class ChannelChatBadgesVersion(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ChannelChatBadgesVersion(BaseSchema): id: str image_url_1x: str image_url_2x: str @@ -541,16 +474,12 @@ class ChannelChatBadgesVersion(BaseModel): click_url: str | None -class ChannelChatBadgesData(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ChannelChatBadgesData(BaseSchema): set_id: str versions: list[ChannelChatBadgesVersion] -class ChannelChatBadges(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ChannelChatBadges(BaseSchema): data: list[ChannelChatBadgesData] @@ -558,9 +487,7 @@ class GlobalChatBadges(ChannelChatBadges): pass -class ChatSettingsData(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ChatSettingsData(BaseSchema): broadcaster_id: int emote_mode: bool follower_mode: bool @@ -574,21 +501,15 @@ class ChatSettingsData(BaseModel): unique_chat_mode: bool -class ChatSettings(BaseModel): - model_config = ConfigDict(extra='forbid') - +class ChatSettings(BaseSchema): data: list[ChatSettingsData] -class SharedChatSessionParticipant(BaseModel): - model_config = ConfigDict(extra='forbid') - +class SharedChatSessionParticipant(BaseSchema): broadcaster_id: int -class SharedChatSessionData(BaseModel): - model_config = ConfigDict(extra='forbid') - +class SharedChatSessionData(BaseSchema): session_id: str host_broadcaster_id: int participants: list[SharedChatSessionParticipant] @@ -596,9 +517,7 @@ class SharedChatSessionData(BaseModel): updated_at: datetime -class SharedChatSession(BaseModel): - model_config = ConfigDict(extra='forbid') - +class SharedChatSession(BaseSchema): data: list[SharedChatSessionData] @@ -606,45 +525,33 @@ class UserEmotes(ChannelEmotes): pagination: Pagination | dict[Any, Any] | None = None -class MessageDropReason(BaseModel): - model_config = ConfigDict(extra='forbid') - +class MessageDropReason(BaseSchema): code: str message: str -class MessageData(BaseModel): - model_config = ConfigDict(extra='forbid') - +class MessageData(BaseSchema): message_id: str is_sent: bool drop_reason: MessageDropReason | None = None -class Message(BaseModel): - model_config = ConfigDict(extra='forbid') - +class Message(BaseSchema): data: list[MessageData] -class UserChatColorData(BaseModel): - model_config = ConfigDict(extra='forbid') - +class UserChatColorData(BaseSchema): user_id: int user_login: str user_name: str color: str -class UserChatColor(BaseModel): - model_config = ConfigDict(extra='forbid') - +class UserChatColor(BaseSchema): data: list[UserChatColorData] -class Clip(BaseModel): - model_config = ConfigDict(extra='forbid') - +class Clip(BaseSchema): id: str url: str embed_url: str @@ -664,8 +571,939 @@ class Clip(BaseModel): is_featured: bool -class Clips(BaseModel): - model_config = ConfigDict(extra='forbid') - +class Clips(BaseSchema): data: list[Clip] pagination: Pagination | dict[Any, Any] | None = None + + +class ClipDownload(BaseSchema): + clip_id: str + landscape_download_url: str | None + portrait_download_url: str | None + + +class ClipsDownloads(BaseSchema): + data: list[ClipDownload] + + +class Conduit(BaseSchema): + id: str + shard_count: int + + +class Conduits(BaseSchema): + data: list[Conduit] + + +class ConduitShardTransportWebhook(BaseSchema): + method: Literal['webhook'] + callback: str + + +class ConduitShardTransportWebsocket(BaseSchema): + method: Literal['websocket'] + session_id: str + connected_at: datetime + disconnected_at: datetime + + +class ConduitShard(BaseSchema): + id: str + status: Literal[ + 'enabled', + 'webhook_callback_verification_pending', + 'webhook_callback_verification_failed', + 'notification_failures_exceeded', + 'websocket_disconnected', + 'websocket_failed_ping_pong', + 'websocket_received_inbound_traffic', + 'websocket_internal_error', + 'websocket_network_timeout', + 'websocket_network_error', + 'websocket_failed_to_reconnect', + ] + transport: ConduitShardTransportWebhook | ConduitShardTransportWebsocket + + +class ConduitShards(BaseSchema): + data: list[ConduitShard] + pagination: Pagination | dict[Any, Any] | None = None + + +class UpdateConduitShardTransportWebhook(TypedDict): + method: Literal['webhook'] + callback: str | None + session_id: str | None + + +class UpdateConduitShardTransportWebsocket(TypedDict): + method: Literal['websocket'] + session_id: str | None + + +class UpdateConduitShard(TypedDict): + id: str + transport: ( + UpdateConduitShardTransportWebhook + | UpdateConduitShardTransportWebsocket + ) + + +class UpdateConduitShardsError(BaseSchema): + id: str + message: str + code: str + + +class UpdateConduitShards(ConduitShards): + errors: list[UpdateConduitShardsError] + + +class ContentClassificationLabelModel(BaseSchema): + id: Literal[ + 'DebatedSocialIssuesAndPolitics', + 'DrugsIntoxication', + 'SexualThemes', + 'ViolentGraphic', + 'Gambling', + 'ProfanityVulgarity', + 'MatureGame', + ] + description: str + name: str + + +class ContentClassificationLabels(BaseSchema): + data: list[ContentClassificationLabelModel] + + +class DropEntitlement(BaseSchema): + id: str + benefit_id: str + timestamp: datetime + user_id: int + game_id: int + fulfillment_status: Literal['CLAIMED', 'FULFILLED'] + last_updated: datetime + + +class DropsEntitlements(BaseSchema): + data: list[DropEntitlement] + pagination: Pagination | dict[Any, Any] | None = None + + +class UpdateDropsEntitlementsData(BaseSchema): + status: Literal[ + 'INVALID_ID', 'NOT_FOUND', 'SUCCESS', 'UNAUTHORIZED', 'UPDATE_FAILED' + ] + ids: list[str] + + +class UpdateDropsEntitlements(BaseSchema): + data: list[UpdateDropsEntitlementsData] + + +class ExtensionConfigurationSegmentData(BaseSchema): + segment: Literal['broadcaster', 'developer', 'global'] + broadcaster_id: int + content: str + version: str + + +class ExtensionConfigurationSegment(BaseSchema): + data: list[ExtensionConfigurationSegmentData] + + +class ExtensionLiveChannel(BaseSchema): + broadcaster_id: int + broadcaster_name: str + game_name: str + game_id: int + title: str + + +class ExtensionLiveChannels(BaseSchema): + data: list[ExtensionLiveChannel] + paginaiton: Pagination | dict[Any, Any] | None = None + + +class ExtensionSecret(BaseSchema): + content: str + active_at: datetime + expires_at: datetime + + +class ExtensionSecretData(BaseSchema): + format_version: int + secrets: list[ExtensionSecret] + + +class ExtensionSecrets(BaseSchema): + data: list[ExtensionSecretData] + + +class ExtensionViewsMobile(BaseSchema): + viewer_url: str + + +class ExtensionViewsPanel(BaseSchema): + viewer_url: str + height: int + can_link_external_content: bool + + +class ExtensionViewsVideoOverlay(BaseSchema): + viewer_url: str + can_link_external_content: bool + + +class ExtensionViewsComponent(BaseSchema): + viewer_url: str + aspect_width: int + aspect_height: int + aspect_ratio_x: int + aspect_ratio_y: int + autoscale: bool + scale_pixels: int + target_height: int + size: int + zoom: bool + zoom_pixels: int + can_link_external_content: bool + + +class ExtensionViews(BaseSchema): + mobile: ExtensionViewsMobile + panel: ExtensionViewsPanel + video_overlay: ExtensionViewsVideoOverlay + component: ExtensionViewsComponent + + +class Extension(BaseSchema): + author_name: str + bits_enabled: bool + can_install: bool + configuration_location: Literal['hosted', 'custom', 'none'] + description: str + eula_tos_url: str + has_chat_support: bool + icon_url: str + icon_urls: dict[str, str] + id: str + name: str + privacy_policy_url: str + request_identity_link: bool + screenshot_urls: list[str] + state: Literal[ + 'Approved', + 'AssetsUploaded', + 'Deleted', + 'Deprecated', + 'InReview', + 'InTest', + 'PendingAction', + 'Rejected', + 'Released', + ] + subscriptions_support_level: Literal['none', 'optional'] + summary: str + support_email: str + version: str + viewer_summary: str + views: ExtensionViews + allowlisted_config_urls: list[str] + allowlisted_panel_urls: list[str] + + +class Extensions(BaseSchema): + data: list[Extension] + + +class ExtensionBitsProductCost(BaseSchema): + amount: int + type: Literal['bits'] + + +class ExtensionBitsProduct(BaseSchema): + sku: str + cost: ExtensionBitsProductCost + in_development: bool + display_name: str + expiration: datetime + is_broadcast: bool + + +class ExtensionBitsProducts(BaseSchema): + data: list[ExtensionBitsProduct] + + +class EventsubBaseSubscriptions(BaseSchema): + data: list[sub.Any] + total: int + total_cost: int + max_total_cost: int + pagination: Pagination | dict[Any, Any] | None = None + + +class EventsubWebsocketDisconnectSubscriptions(EventsubBaseSubscriptions): + disconnected_at: datetime + + +class EventsubWebsocketConnectSubscriptions(EventsubBaseSubscriptions): + connected_at: datetime + + +class EventsubSubscription(BaseSchema): + data: ( + EventsubWebsocketDisconnectSubscriptions + | EventsubWebsocketConnectSubscriptions + | EventsubBaseSubscriptions + ) + + +class EventsubSubscriptions(BaseSchema): + data: list[ + EventsubSubscription + | EventsubWebsocketConnectSubscriptions + | EventsubBaseSubscriptions + ] + + +class Game(BaseSchema): + id: int + name: str + box_art_url: str + igdb_id: int | str + + +class Games(BaseSchema): + data: list[Game] + paginaiton: Pagination | dict[Any, Any] | None = None + + +class CreatorGoal(BaseSchema): + id: str + broadcaster_id: int + broadcaster_login: str + broadcaster_name: str + type: Literal[ + 'follower', + 'subscription', + 'subscription_count', + 'new_subscription', + 'new_subscription_count', + ] + description: str + current_amount: int + target_amount: int + created_at: datetime + + +class CreatorGoals(BaseSchema): + data: list[CreatorGoal] + + +class TopContribution(BaseSchema): + user_id: int + user_login: str + user_name: str + type: Literal['bits', 'other', 'subscription'] + total: int + + +class SharedTrainParticipant(BaseSchema): + broadcaster_user_id: int + broadcaster_user_login: str + broadcaster_user_name: str + + +class HypeTrainCurrent(BaseSchema): + id: str + broadcaster_user_id: int + broadcaster_user_login: str + broadcaster_user_name: str + level: int + total: int + progress: int + goal: int + top_contributions: list[TopContribution] + shared_train_participants: list[SharedTrainParticipant] + started_at: datetime + expires_at: datetime + type: Literal['trasure', 'golden_kappa', 'regular'] + + +class HypeTrainAllTimeHigh(BaseModel): + level: int + total: int + achieved_at: datetime + + +class HypeTrainSharedAllTimeHigh(BaseModel): + level: int + total: int + achieved_at: datetime + + +class HypeTrain(BaseSchema): + current: HypeTrainCurrent + all_time_high: HypeTrainAllTimeHigh + shared_all_time_high: HypeTrainSharedAllTimeHigh + + +class HypeTrainStatus(BaseSchema): + data: list[HypeTrain] + + +class AutomodStatusData(BaseSchema): + msg_id: int + is_permitted: bool + + +class AutomodStatus(BaseSchema): + data: list[AutomodStatusData] + + +class AutomodSettingsData(BaseSchema): + broadcaster_id: int + moderator_id: int + overall_level: int | None + disability: int + aggression: int + sexuality_sex_or_gender: int + misogyny: int + bullying: int + swearing: int + race_ethnicity_or_religion: int + sex_based_terms: int + + +class AutomodSettings(BaseSchema): + data: list[AutomodSettingsData] + + +class BannedUser(BaseSchema): + user_id: int + user_login: str + user_name: str + expires_at: datetime | str + created_at: datetime + reason: str + moderator_id: int + moderator_login: str + moderator_name: str + + +class BannedUsers(BaseSchema): + data: list[BannedUser] + pagination: Pagination | dict[Any, Any] | None = None + + +class BanUserData(BaseSchema): + broadcaster_id: int + moderator_id: int + user_id: int + created_at: datetime + end_time: datetime | None + + +class BanUser(BaseSchema): + data: list[BanUserData] + + +class UnbanRequest(BaseSchema): + id: str + broadcaster_id: int + broadcaster_login: str + broadcaster_name: str + moderator_id: int + moderator_login: str + moderator_name: str + user_id: int + user_login: str + user_name: str + text: str + status: Literal[ + 'approved', 'denied', 'pending', 'acknowledged', 'canceled' + ] + created_at: datetime + resolved_at: datetime | None + resolution_text: str | None + + +class UnbanRequests(BaseSchema): + data: list[UnbanRequest] + pagination: Pagination | dict[Any, Any] | None = None + + +class BlockedTerm(BaseSchema): + broadcaster_id: int + moderator_id: int + id: str + text: str + created_at: datetime + updated_at: datetime + expires_at: datetime | None + + +class BlockedTerms(BaseSchema): + data: list[BlockedTerm] + pagination: Pagination | dict[Any, Any] | None = None + + +class ModeratedChannel(BaseSchema): + broadcaster_id: int + broadcaster_login: str + broadcaster_name: str + + +class ModeratedChannels(BaseSchema): + data: list[ModeratedChannel] + pagination: Pagination | dict[Any, Any] | None = None + + +class Moderator(BaseSchema): + user_id: int + user_login: str + user_name: str + + +class Moderators(BaseSchema): + data: list[Moderator] + pagination: Pagination | dict[Any, Any] | None = None + + +class VIP(BaseSchema): + user_id: int + user_login: str + user_name: str + + +class VIPs(BaseSchema): + data: list[VIP] + pagination: Pagination | dict[Any, Any] | None = None + + +class ShieldModeStatusData(BaseSchema): + moderator_id: int + moderator_login: str + moderator_name: str + is_active: bool + last_activated_at: datetime + + +class ShieldModeStatus(BaseSchema): + data: list[ShieldModeStatusData] + + +class UserWarn(BaseSchema): + broadcaster_id: int + user_id: int + moderator_id: int + reason: str + + +class UserWarns(BaseSchema): + data: list[UserWarn] + + +class PollChoice(BaseSchema): + id: str + title: str + votes: int + channel_points_votes: int + + +class Poll(BaseSchema): + id: str + broadcaster_id: int + broadcaster_name: str + broadcaster_login: str + title: str + choices: list[PollChoice] + channel_points_voting_enabled: bool + channel_points_per_vote: int + status: Literal[ + 'ACTIVE', 'COMPLETED', 'TERMINATED', 'ARCHIVED', 'MODERATED', 'INVALID' + ] + duration: int + started_at: datetime + ended_at: datetime | None + + +class Polls(BaseSchema): + data: list[Poll] + pagination: Pagination | dict[Any, Any] | None = None + + +class PredictionTopPredictor(BaseSchema): + user_id: int + user_login: str + user_name: str + channel_points_used: int + channel_points_won: int + + +class PredictionOutcome(BaseSchema): + id: str + title: str + users: int + channel_points: int + top_predictors: list[PredictionTopPredictor] + color: Literal['BLUE', 'PINK'] + + +class Prediction(BaseSchema): + id: str + broadcaster_id: int + broadcaster_name: str + broadcaster_login: str + title: str + winning_outcome_id: str | None + outcomes: list[PredictionOutcome] + prediction_window: int + status: Literal['ACTIVE', 'CANCELED', 'LOCKED', 'RESOLVED'] + created_at: datetime + ended_at: datetime | None + locked_at: datetime | None + + +class Predictions(BaseSchema): + data: list[Prediction] + pagination: Pagination | dict[Any, Any] | None = None + + +class Raid(BaseSchema): + created_at: datetime + is_mature: bool + + +class Raids(BaseSchema): + data: list[Raid] + + +class ScheduleVacation(BaseSchema): + start_time: datetime + end_time: datetime + + +class ScheduleSegmentCategory(BaseSchema): + id: str + name: str + + +class ScheduleSegment(BaseSchema): + id: str + start_time: datetime + end_time: datetime + title: str + canceled_until: datetime | None + category: ScheduleSegmentCategory + is_recurring: bool + + +class Schedule(BaseSchema): + broadcaster_id: int + broadcaster_login: str + broadcaster_name: str + vacation: ScheduleVacation | None + segments: list[ScheduleSegment] + + +class Schedules(BaseSchema): + data: list[Schedule] + pagination: Pagination | dict[Any, Any] | None = None + + +class Category(BaseSchema): + id: int | str + name: str + box_art_url: str + + +class Categories(BaseSchema): + data: list[Category] + pagination: Pagination | dict[Any, Any] | None = None + + +class Channel(BaseSchema): + broadcaster_language: str + broadcaster_login: str + display_name: str + game_id: int | str + game_name: str + id: int + is_live: bool + tags: list[str] + thumbnail_url: str + title: str + started_at: datetime | None + + +class Channels(BaseSchema): + data: list[Channel] + pagination: Pagination | dict[Any, Any] | None = None + + +class StreamKey(BaseSchema): + stream_key: str + + +class StreamKeys(BaseSchema): + data: list[StreamKey] + + +class Stream(BaseSchema): + id: int + user_id: int + user_name: str + user_login: str + game_id: int | str + game_name: str + type: Literal['live'] + title: str + tags: list[str] + viewer_count: int + started_at: datetime + language: str + thumbnail_url: str + is_mature: bool + + +class Streams(BaseSchema): + data: list[Stream] + pagination: Pagination | dict[Any, Any] | None = None + + +class BaseStreamMarker(BaseSchema): + id: str + created_at: datetime + description: str + position_seconds: int + + +class StreamMarker(BaseStreamMarker): + URL: str + + +class CreateStreamMarkers(BaseSchema): + data: list[BaseStreamMarker] + + +class StreamMarkerVideo(BaseSchema): + video_id: int + markers: list[StreamMarker] + + +class StreamMarkersData(BaseSchema): + user_id: int + user_login: str + user_name: str + videos: list[StreamMarkerVideo] + + +class StreamMarkers(BaseSchema): + data: list[StreamMarkersData] + pagination: Pagination | dict[Any, Any] | None = None + + +class Subscription(BaseSchema): + broadcaster_id: int + broadcaster_login: str + broadcaster_name: str + gifter_id: int | None = None + gifter_login: str | None = None + gifter_name: str | None = None + is_gift: bool + tier: Literal['1000', '2000', '3000'] + + +class BroadcasterSubscription(Subscription): + plan_name: str + user_id: int + user_login: str + user_name: str + + +class BroadcasterSubscriptions(BaseSchema): + data: list[BroadcasterSubscription] + pagination: Pagination | dict[Any, Any] | None = None + total: int + points: int + + +class UserSubscription(BaseSchema): + data: list[Subscription] + + +class ChannelTeam(BaseSchema): + id: int + broadcaster_id: int + broadcaster_login: str + broadcaster_name: str + background_image_url: str | None + banner: str | None + created_at: datetime + updated_at: datetime + info: str + thumbnail_url: str + team_name: str + team_display_name: str + + +class ChannelTeams(BaseSchema): + data: list[ChannelTeam] + + +class TeamUser(BaseSchema): + user_id: int + user_login: str + user_name: str + + +class Team(BaseSchema): + id: int + users: list[TeamUser] + background_image_url: str | None + banner: str | None + created_at: datetime + updated_at: datetime + info: str + thumbnail_url: str + team_name: str + team_display_name: str + + +class Teams(BaseSchema): + data: list[Team] + + +class User(BaseSchema): + id: int + login: str + display_name: str + type: Literal['admin', 'global_mod', 'staff', ''] + broadcaster_type: Literal['partner', 'affiliate', ''] + description: str | None + profile_image_url: str + offline_image_url: str + email: str | None = None + created_at: datetime + + +class Users(BaseSchema): + data: list[User] + + +class AuthorizationByUser(BaseSchema): + user_id: int + user_login: str + user_name: str + scopes: list[scopes.Any] + + +class AuthorizationsByUser(BaseSchema): + data: list[AuthorizationByUser] + + +class UserBlock(BaseSchema): + user_id: int + user_login: str + display_name: str + + +class UserBlockList(BaseSchema): + data: list[UserBlock] + + +class UserExtension(BaseSchema): + id: str + version: str + name: str + can_activate: bool + type: list[Literal['component', 'mobile', 'overlay', 'panel']] + + +class UserExtensions(BaseSchema): + data: list[UserExtension] + + +class UserActiveExtensionPanelActive(BaseSchema): + active: Literal[True] + id: str + version: str + name: str + + +class UserActiveExtensionPanelInactive(BaseSchema): + active: Literal[False] + + +class UserActiveExtensionOverlayActive(UserActiveExtensionPanelActive): + pass + + +class UserActiveExtensionOverlayInactive(UserActiveExtensionPanelInactive): + pass + + +class UserActiveExtensionComponentActive(UserActiveExtensionPanelActive): + x: int + y: int + + +class UserActiveExtensionComponentInactive(UserActiveExtensionPanelInactive): + pass + + +class UserActiveExtension(BaseSchema): + panel: dict[ + int, UserActiveExtensionPanelActive | UserActiveExtensionPanelInactive + ] + overlay: dict[ + int, + UserActiveExtensionOverlayActive | UserActiveExtensionOverlayInactive, + ] + component: dict[ + int, + UserActiveExtensionComponentActive + | UserActiveExtensionComponentInactive, + ] + + +class UserActiveExtensions(BaseSchema): + data: list[UserActiveExtension] + + +class VideoMutedSegment(BaseSchema): + duration: int + offset: int + + +class Video(BaseSchema): + id: int + stream_id: str | None + user_id: int + user_login: str + user_name: str + title: str + description: str + created_at: datetime + published_at: datetime + url: str + thumbnail_url: str + viewable: Literal['public'] + view_count: int + language: str + type: Literal['archive', 'highlight', 'upload'] + duration: str + muted_segments: list[VideoMutedSegment] + + +class Videos(BaseSchema): + data: list[Video] + pagination: Pagination | dict[Any, Any] | None + + +class DeleteVideos(BaseSchema): + data: list[int] diff --git a/src/twitchclient/scopes.py b/src/twitchclient/scopes.py new file mode 100644 index 0000000..9479f50 --- /dev/null +++ b/src/twitchclient/scopes.py @@ -0,0 +1,168 @@ +from typing import Literal + +# Analytics +ANALYTICS_READ_EXTENSIONS = Literal['analytics:read:extensions'] +ANALYTICS_READ_GAMES = Literal['analytics:read:games'] + +# Bits +BITS_READ = Literal['bits:read'] + +# Channel +CHANNEL_MANAGE_ADS = Literal['channel:manage:ads'] +CHANNEL_READ_ADS = Literal['channel:read:ads'] +CHANNEL_MANAGE_BROADCAST = Literal['channel:manage:broadcast'] +CHANNEL_READ_CHARITY = Literal['channel:read:charity'] +CHANNEL_EDIT_COMMERCIAL = Literal['channel:edit:commercial'] +CHANNEL_READ_EDITORS = Literal['channel:read:editors'] +CHANNEL_MANAGE_EXTENSIONS = Literal['channel:manage:extensions'] +CHANNEL_READ_GOALS = Literal['channel:read:goals'] +CHANNEL_READ_GUEST_STAR = Literal['channel:read:guest_star'] +CHANNEL_MANAGE_GUEST_STAR = Literal['channel:manage:guest_star'] +CHANNEL_READ_HYPE_TRAIN = Literal['channel:read:hype_train'] +CHANNEL_MANAGE_MODERATORS = Literal['channel:manage:moderators'] +CHANNEL_READ_POLLS = Literal['channel:read:polls'] +CHANNEL_MANAGE_POLLS = Literal['channel:manage:polls'] +CHANNEL_READ_PREDICTIONS = Literal['channel:read:predictions'] +CHANNEL_MANAGE_PREDICTIONS = Literal['channel:manage:predictions'] +CHANNEL_MANAGE_RAIDS = Literal['channel:manage:raids'] +CHANNEL_READ_REDEMPTIONS = Literal['channel:read:redemptions'] +CHANNEL_MANAGE_REDEMPTIONS = Literal['channel:manage:redemptions'] +CHANNEL_MANAGE_SCHEDULE = Literal['channel:manage:schedule'] +CHANNEL_READ_STREAM_KEY = Literal['channel:read:stream_key'] +CHANNEL_READ_SUBSCRIPTIONS = Literal['channel:read:subscriptions'] +CHANNEL_MANAGE_VIDEOS = Literal['channel:manage:videos'] +CHANNEL_READ_VIPS = Literal['channel:read:vips'] +CHANNEL_MANAGE_VIPS = Literal['channel:manage:vips'] +CHANNEL_BOT = Literal['channel:bot'] +CHANNEL_MODERATE = Literal['channel:moderate'] + +# clips +CLIPS_EDIT = Literal['clips:edit'] + +# Moderation +MODERATION_READ = Literal['moderation:read'] +MODERATOR_MANAGE_ANNOUNCEMENTS = Literal['moderator:manage:announcements'] +MODERATOR_MANAGE_AUTOMOD = Literal['moderator:manage:automod'] +MODERATOR_READ_AUTOMOD_SETTINGS = Literal['moderator:read:automod_settings'] +MODERATOR_MANAGE_AUTOMOD_SETTINGS = Literal[ + 'moderator:manage:automod_settings' +] +MODERATOR_MANAGE_BANNED_USERS = Literal['moderator:manage:banned_users'] +MODERATOR_READ_BLOCKED_TERMS = Literal['moderator:read:blocked_terms'] +MODERATOR_MANAGE_BLOCKED_TERMS = Literal['moderator:manage:blocked_terms'] +MODERATOR_MANAGE_CHAT_MESSAGES = Literal['moderator:manage:chat_messages'] +MODERATOR_READ_CHAT_SETTINGS = Literal['moderator:read:chat_settings'] +MODERATOR_MANAGE_CHAT_SETTINGS = Literal['moderator:manage:chat_settings'] +MODERATOR_READ_CHATTERS = Literal['moderator:read:chatters'] +MODERATOR_READ_FOLLOWERS = Literal['moderator:read:followers'] +MODERATOR_READ_GUEST_STAR = Literal['moderator:read:guest_star'] +MODERATOR_MANAGE_GUEST_STAR = Literal['moderator:manage:guest_star'] +MODERATOR_READ_SHIELD_MODE = Literal['moderator:read:shield_mode'] +MODERATOR_MANAGE_SHIELD_MODE = Literal['moderator:manage:shield_mode'] +MODERATOR_READ_SHOUTOUTS = Literal['moderator:read:shoutouts'] +MODERATOR_MANAGE_SHOUTOUTS = Literal['moderator:manage:shoutouts'] +MODERATOR_READ_UNBAN_REQUESTS = Literal['moderator:read:unban_requests'] +MODERATOR_MANAGE_UNBAN_REQUESTS = Literal['moderator:manage:unban_requests'] +MODERATOR_READ_WARNINGS = Literal['moderator:read:warnings'] +MODERATOR_MANAGE_WARNINGS = Literal['moderator:manage:warnings'] + +# User +USER_EDIT = Literal['user:edit'] +USER_EDIT_FOLLOWS = Literal['user:edit:follows'] +USER_READ_BLOCKED_USERS = Literal['user:read:blocked_users'] +USER_MANAGE_BLOCKED_USERS = Literal['user:manage:blocked_users'] +USER_READ_BROADCAST = Literal['user:read:broadcast'] +USER_MANAGE_CHAT_COLOR = Literal['user:manage:chat_color'] +USER_READ_EMAIL = Literal['user:read:email'] +USER_READ_EMOTES = Literal['user:read:emotes'] +USER_READ_FOLLOWS = Literal['user:read:follows'] +USER_READ_MODERATED_CHANNELS = Literal['user:read:moderated_channels'] +USER_READ_SUBSCRIPTIONS = Literal['user:read:subscriptions'] +USER_MANAGE_WHISPERS = Literal['user:manage:whispers'] +USER_BOT = Literal['user:bot'] +USER_READ_CHAT = Literal['user:read:chat'] +USER_WRITE_CHAT = Literal['user:write:chat'] + +# Chat +CHAT_EDIT = Literal['chat:edit'] +CHAT_READ = Literal['chat:read'] + +# Whispers +WHISPERS_READ = Literal['whispers:read'] +WHISPERS_EDIT = Literal['whispers:edit'] + +Any = ( + ANALYTICS_READ_EXTENSIONS + | ANALYTICS_READ_GAMES + | BITS_READ + | CHANNEL_MANAGE_ADS + | CHANNEL_READ_ADS + | CHANNEL_MANAGE_BROADCAST + | CHANNEL_READ_CHARITY + | CHANNEL_EDIT_COMMERCIAL + | CHANNEL_READ_EDITORS + | CHANNEL_MANAGE_EXTENSIONS + | CHANNEL_READ_GOALS + | CHANNEL_READ_GUEST_STAR + | CHANNEL_MANAGE_GUEST_STAR + | CHANNEL_READ_HYPE_TRAIN + | CHANNEL_MANAGE_MODERATORS + | CHANNEL_READ_POLLS + | CHANNEL_MANAGE_POLLS + | CHANNEL_READ_PREDICTIONS + | CHANNEL_MANAGE_PREDICTIONS + | CHANNEL_MANAGE_RAIDS + | CHANNEL_READ_REDEMPTIONS + | CHANNEL_MANAGE_REDEMPTIONS + | CHANNEL_MANAGE_SCHEDULE + | CHANNEL_READ_STREAM_KEY + | CHANNEL_READ_SUBSCRIPTIONS + | CHANNEL_MANAGE_VIDEOS + | CHANNEL_READ_VIPS + | CHANNEL_MANAGE_VIPS + | CHANNEL_BOT + | CHANNEL_MODERATE + | CLIPS_EDIT + | MODERATION_READ + | MODERATOR_MANAGE_ANNOUNCEMENTS + | MODERATOR_MANAGE_AUTOMOD + | MODERATOR_READ_AUTOMOD_SETTINGS + | MODERATOR_MANAGE_AUTOMOD_SETTINGS + | MODERATOR_MANAGE_BANNED_USERS + | MODERATOR_READ_BLOCKED_TERMS + | MODERATOR_MANAGE_BLOCKED_TERMS + | MODERATOR_MANAGE_CHAT_MESSAGES + | MODERATOR_READ_CHAT_SETTINGS + | MODERATOR_MANAGE_CHAT_SETTINGS + | MODERATOR_READ_CHATTERS + | MODERATOR_READ_FOLLOWERS + | MODERATOR_READ_GUEST_STAR + | MODERATOR_MANAGE_GUEST_STAR + | MODERATOR_READ_SHIELD_MODE + | MODERATOR_MANAGE_SHIELD_MODE + | MODERATOR_READ_SHOUTOUTS + | MODERATOR_MANAGE_SHOUTOUTS + | MODERATOR_READ_UNBAN_REQUESTS + | MODERATOR_MANAGE_UNBAN_REQUESTS + | MODERATOR_READ_WARNINGS + | MODERATOR_MANAGE_WARNINGS + | USER_EDIT + | USER_EDIT_FOLLOWS + | USER_READ_BLOCKED_USERS + | USER_MANAGE_BLOCKED_USERS + | USER_READ_BROADCAST + | USER_MANAGE_CHAT_COLOR + | USER_READ_EMAIL + | USER_READ_EMOTES + | USER_READ_FOLLOWS + | USER_READ_MODERATED_CHANNELS + | USER_READ_SUBSCRIPTIONS + | USER_MANAGE_WHISPERS + | USER_BOT + | USER_READ_CHAT + | USER_WRITE_CHAT + | CHAT_EDIT + | CHAT_READ + | WHISPERS_READ + | WHISPERS_EDIT +) -- 2.47.2