commit da232d7461268efbbda354f78dcd8896fffaab3d Author: Miwory Date: Wed Feb 25 09:53:19 2026 +0300 Релиз diff --git a/.gitea/workflows/dev.yaml b/.gitea/workflows/dev.yaml new file mode 100644 index 0000000..4f2f8cf --- /dev/null +++ b/.gitea/workflows/dev.yaml @@ -0,0 +1,58 @@ +name: Verify Dev Build +run-name: ${{ github.actor }} verifying dev build +on: + push: + branches: + - dev + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cache uv binary + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/uv + key: uv-${{ runner.os }} + restore-keys: uv-${{ runner.os }} + + - name: Cache uv dependencies + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/.cache/uv + key: uv-${{ runner.os }} + restore-keys: uv-${{ runner.os }} + + - name: Cache pre-commit + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-cache-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: pre-commit-cache-${{ runner.os }}- + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: "0.10.5" + enable-cache: true + cache-local-path: ${{ github.workspace }}/.cache/uv + tool-dir: ${{ github.workspace }}/.cache/uv + tool-bin-dir: ${{ github.workspace }}/.cache/uv + cache-dependency-glob: "" + + - name: Set up Python + run: uv python install + + - name: Install the project + run: uv sync --all-extras --no-install-project --cache-dir ${{ github.workspace }}/.cache/uv + + - name: Linter & Formatter + run: uv run pre-commit run --all-files + + - name: Build Package + run: uv build --cache-dir ${{ github.workspace }}/.cache/uv diff --git a/.gitea/workflows/latest.yaml b/.gitea/workflows/latest.yaml new file mode 100644 index 0000000..4783bca --- /dev/null +++ b/.gitea/workflows/latest.yaml @@ -0,0 +1,64 @@ +name: Build And Publish Package +run-name: ${{ github.actor }} builds and publishes package to PyPI +on: + push: + branches: + - latest + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Cache uv binary + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/uv + key: uv-${{ runner.os }} + restore-keys: uv-${{ runner.os }} + + - name: Cache uv dependencies + uses: actions/cache@v4 + with: + path: ${{ github.workspace }}/.cache/uv + key: uv-${{ runner.os }} + restore-keys: uv-${{ runner.os }} + + - name: Cache pre-commit + uses: actions/cache@v4 + with: + path: ~/.cache/pre-commit + key: pre-commit-cache-${{ runner.os }}-${{ hashFiles('.pre-commit-config.yaml') }} + restore-keys: pre-commit-cache-${{ runner.os }}- + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + version: "0.10.5" + enable-cache: true + cache-local-path: ${{ github.workspace }}/.cache/uv + tool-dir: ${{ github.workspace }}/.cache/uv + tool-bin-dir: ${{ github.workspace }}/.cache/uv + cache-dependency-glob: "" + + - name: Set up Python + run: uv python install + + - name: Install the project + run: uv sync --all-extras --no-install-project --cache-dir ${{ github.workspace }}/.cache/uv + + - name: Linter & Formatter + run: uv run pre-commit run --all-files + + - name: Build Package + run: uv build --cache-dir ${{ github.workspace }}/.cache/uv + + - name: Publish to Gitea PyPI + run: | + uv publish \ + --index OxideTwitch \ + --token ${{ secrets.CI_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d48f6de --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv + +# Ruff +.ruff_cache + +# uv +uv.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5eb171f --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,34 @@ +repos: + - repo: https://github.com/crate-ci/typos + rev: v1.42.0 + hooks: + - id: typos + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.0 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format + + - repo: https://github.com/RobertCraigie/pyright-python + rev: v1.1.408 + hooks: + - id: pyright + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: check-docstring-first + - id: check-added-large-files + - id: check-yaml + - id: debug-statements + - id: check-merge-conflict + - id: double-quote-string-fixer + - id: end-of-file-fixer + + - repo: meta + hooks: + - id: check-hooks-apply + - id: check-useless-excludes diff --git a/README.md b/README.md new file mode 100644 index 0000000..64c8e18 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# 🟣 OxideTwitch + +**OxideTwitch** is a specialized Twitch API client built on the [OxideHTTP](https://git.miwory.dev/OxideHTTP/OxideHTTP) core. It combines the speed of Rust with an easy-to-use Pythonic interface, specifically tuned for the rigors of the Twitch Developer ecosystem. + +--- + +## 🔥 Why OxideTwitch? + +Twitch's API has strict rate limits and requires constant token management. OxideTwitch handles the heavy lifting for you: + +* **⚡ Oxide Core:** Uses `pyreqwest` (Rust) for underlying HTTP calls. +* **🛡️ Distributed Rate Limiting:** Uses your OxideHTTP Redis integration to ensure your bot never hits a `429 Too Many Requests` even across multiple instances. +* **💾 Intelligent Caching:** Automatically caches common lookups (like User IDs or Stream Status) to save your API quota. +* **🏗️ Type-Safe Models:** Fully validated responses using Pydantic models. + +--- + +## 📦 Installation + +Ensure you have your Gitea index configured in `uv`, then run: + +```bash +uv add oxidetwitch + +``` + +--- + +## 🛠 Quick Start + +### Basic User Lookup + +OxideTwitch automatically handles the `base_url` and header injection for you. + +```python +import asyncio +from oxidetwitch.api import TwitchAPIClient + +async def main(): + async with TwitchClient( + client_id="your_id", + client_secret="your_client_secret", + redis_url="redis://localhost:6379", + ) as twitch: + # Get user data (automatically cached if configured) + users = await twitch.get_users(access_token="access_token", login="Miwowy") + user = users.data[0] + print(f"User ID: {user.id} | Description: {user.description}") + +asyncio.run(main()) + +``` + +### Handling Streams with Rate Limiting + +If you are polling 100+ streams, OxideTwitch spaces out the requests using the **GCRA algorithm** to keep your token healthy. + +```python +async def poll_streams(channels): + async with TwitchClient(...) as twitch: + # These will be executed as fast as the rate limiter allows + tasks = [twitch.get_stream(user_login=name) for name in channels] + streams = await asyncio.gather(*tasks) + return [s for s in streams if s.is_live] + +``` + +--- + +## ⚙️ Advanced: Using with `uv` and Gitea + +Since **OxideTwitch** depends on **OxideHTTP**, ensure your `pyproject.toml` is configured to find both in your private registry: + +```toml +[[tool.uv.index]] +name = "OxideTwitch" +url = "https://git.miwory.dev/api/packages/OxideHTTP/pypi/simple" + +``` + +--- diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..acf59a1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,104 @@ +[project] +name = "oxidetwitch" +version = "1.0.0" +description = "Client for Twitch API" +readme = "README.md" +authors = [{ name = "Miwory", email = "miwory.uwu@gmail.com" }] +requires-python = ">=3.14" +dependencies = ["oxidehttp>=1.0.3,<=2.0.0", "pydantic>=2.12,<=2.13"] + +[build-system] +requires = ["uv_build>=0.9.2,<0.10.0"] +build-backend = "uv_build" + +[dependency-groups] +dev = [ + "ty>=0.0.17", + "ruff>=0.15.0", + "pyright>=1.1.408", + "poethepoet>=0.40.0", + "pre-commit>=4.5.1", +] + +[[tool.uv.index]] +name = "OxideHTTP" +url = "https://git.miwory.dev/api/packages/OxideHTTP/pypi/simple" + +[[tool.uv.index]] +name = "OxideTwitch" +url = "https://git.miwory.dev/api/packages/OxideHTTP/pypi/simple" +publish-url = "https://git.miwory.dev/api/packages/OxideHTTP/pypi" +explicit = true + +[tool.poe.tasks] +_git = "git add ." +_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" +strictListInference = true +strictDictionaryInference = true +strictSetInference = true +deprecateTypingAliases = true +typeCheckingMode = "strict" +pythonPlatform = "All" +stubPath = "typings" + +[tool.ruff] +target-version = "py313" +line-length = 79 +fix = true + +[tool.ruff.lint] +preview = true +select = [ + "E", + "W", + "F", + "UP", + "A", + "B", + "C4", + "SIM", + "I", + "S", + "G", + "FAST", + "ASYNC", + "BLE", + "INT", + "ISC", + "ICN", + "PYI", + "INP", + "RSE", + "PIE", + "SLOT", + "TID", + "LOG", + "FBT", + "DTZ", + "EM", + "PERF", + "RUF", +] +ignore = ["RUF002", "RUF029", "S101", "S104", "W505"] + +[tool.ruff.lint.pydoclint] +ignore-one-line-docstrings = true + +[tool.ruff.format] +quote-style = "single" +indent-style = "space" +docstring-code-format = true + +[tool.uv.sources] +aiohttpx = { index = "Miwory" } diff --git a/src/oxidetwitch/__init__.py b/src/oxidetwitch/__init__.py new file mode 100644 index 0000000..b794fd4 --- /dev/null +++ b/src/oxidetwitch/__init__.py @@ -0,0 +1 @@ +__version__ = '0.1.0' diff --git a/src/oxidetwitch/api.py b/src/oxidetwitch/api.py new file mode 100644 index 0000000..9552dbf --- /dev/null +++ b/src/oxidetwitch/api.py @@ -0,0 +1,4412 @@ +from datetime import datetime +from typing import Literal +from zoneinfo import ZoneInfo + +from oxidehttp import status as st +from oxidehttp.client import OxideHTTP + +from . import schema as s +from .eventsub import statuses as sub_status +from .eventsub import types as sub_type + + +class TwitchAPIClient(OxideHTTP): + def __init__( + self, + client_id: str, + client_secret: str, + redirect_uri: str, + redis_url: str | None = None, + proxy_url: str | None = None, + ) -> None: + self.base_uri = 'https://api.twitch.tv/helix' + self.client_id = client_id + self.client_secret = client_secret + self.redirect_uri = redirect_uri + + super().__init__( + base_url=self.base_uri, + headers={'Client-Id': self.client_id}, + redis_url=redis_url, + ratelimit_key='twitch' if redis_url else None, + ratelimit_limit=700 if redis_url else None, + proxy_url=proxy_url, + ) + + async def start_commercial( + self, access_token: str, broadcaster_id: int, length: int + ) -> s.StartCommercial: + req = await self.post( + '/channels/commercial', + headers={'Authorization': f'Bearer {access_token}'}, + json={ + 'broadcaster_id': broadcaster_id, + 'length': length, + }, + ) + + match req.status_code: + case st.OK: + return s.StartCommercial.model_validate(await req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.NOT_FOUND + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_ad_schedule( + self, access_token: str, broadcaster_id: int + ) -> s.AdSchedule: + req = await self.get( + '/channels/ads', + headers={'Authorization': f'Bearer {access_token}'}, + params={ + 'broadcaster_id': broadcaster_id, + }, + ) + + match req.status_code: + case st.OK: + return s.AdSchedule.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def snooze_next_ad( + self, access_token: str, broadcaster_id: int + ) -> s.SnoozeNextAd: + req = await self.post( + '/channels/ads/schedule/snooze', + headers={'Authorization': f'Bearer {access_token}'}, + params={ + 'broadcaster_id': broadcaster_id, + }, + ) + + match req.status_code: + case st.OK: + return s.SnoozeNextAd.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + 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, + ended_at: datetime | None = None, + first: int = 20, + after: str | None = None, + ) -> s.ExtensionAnalytics: + req = await self.get( + '/analytics/extensions', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'extension_id': extension_id, + 'type': analytics_type, + 'started_at': started_at, + 'ended_at': ended_at, + 'first': first, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.ExtensionAnalytics.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + 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, + ended_at: datetime | None = None, + first: int = 20, + after: str | None = None, + ) -> s.GameAnalytics: + req = await self.get( + '/analytics/games', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'game_id': game_id, + 'type': analytics_type, + 'started_at': started_at, + 'ended_at': ended_at, + 'first': first, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.GameAnalytics.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + 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, + ) -> s.BitsLeaderboard: + req = await self.get( + '/bits/leaderboard', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'count': count, + 'period': period, + 'started_at': started_at, + 'user_id': user_id, + } + ), + ) + + match req.status_code: + case st.OK: + return s.BitsLeaderboard.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_cheermotes( + self, access_token: str, *, broadcaster_id: int | None = None + ) -> s.Cheermotes: + req = await self.get( + '/bits/cheermotes', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict({'broadcaster_id': broadcaster_id}), + ) + + match req.status_code: + case st.OK: + return s.Cheermotes.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + 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, + ) -> s.ExtensionTransactions: + req = await self.get( + '/extensions/transactions', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'extension_id': extension_id, + 'user_id': user_id, + 'first': first, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.ExtensionTransactions.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_channel_information( + self, + access_token: str, + broadcaster_id: int | list[int], + ) -> s.ChannelsInformation: + req = await self.get( + '/channels', + headers={'Authorization': f'Bearer {access_token}'}, + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.ChannelsInformation.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def modify_channel_information( + self, + access_token: str, + broadcaster_id: int, + *, + game_id: int | None = None, + broadcaster_language: str | None = None, + title: str | None = None, + delay: int | None = None, + tags: list[str] | None = None, + content_classification_labels: list[s.ContentClassificationLabel] + | None = None, + is_branded_content: bool | None = None, + ) -> bool: + data = self.clean_dict( + { + 'game_id': game_id, + 'broadcaster_language': broadcaster_language, + 'title': title, + 'delay': delay, + 'tags': tags, + 'content_classification_labels': content_classification_labels, + 'is_branded_content': is_branded_content, + } + ) + req = await self.patch( + '/channels', + headers={'Authorization': f'Bearer {access_token}'}, + params={'broadcaster_id': broadcaster_id}, + json=data, + ) + + match req.status_code: + case st.NO_CONTENT: + return True + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_channel_editors( + self, access_token: str, broadcaster_id: int + ) -> s.ChannelEditors: + req = await self.get( + '/channels/editors', + headers={'Authorization': f'Bearer {access_token}'}, + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.ChannelEditors.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_followed_channels( + self, + access_token: str, + broadcaster_id: int, + *, + first: int = 20, + after: str | None = None, + ) -> s.FollowedChannels: + req = await self.get( + '/channels/followed', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'first': first, + 'after': after, + } + ), + ) + + match req.status_code: + case st.OK: + return s.FollowedChannels.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_channel_followers( + self, + access_token: str, + broadcaster_id: int, + *, + user_id: int | None = None, + first: int = 20, + after: str | None = None, + ) -> s.ChannelFollowers: + req = await self.get( + '/channels/followers', + headers={'Authorization': f'Bearer {access_token}'}, + 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.ChannelFollowers.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def create_custom_rewards( + self, + access_token: str, + broadcaster_id: int, + title: str, + cost: int, + *, + prompt: str | None = None, + is_enabled: bool = True, + background_color: str | None = None, + is_user_input_required: bool = False, + is_max_per_stream_enabled: bool = False, + max_per_stream: int | None = None, + is_max_per_user_per_stream_enabled: bool = False, + max_per_user_per_stream: int | None = None, + is_global_cooldown_enabled: bool = False, + global_cooldown_seconds: int | None = None, + should_redemptions_skip_request_queue: bool = False, + ) -> s.CustomRewards: + data = self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'title': title, + 'cost': cost, + 'prompt': prompt, + 'is_enabled': is_enabled, + 'background_color': background_color, + 'is_user_input_required': is_user_input_required, + 'is_max_per_stream_enabled': is_max_per_stream_enabled, + 'max_per_stream': max_per_stream, + 'is_max_per_user_per_' + 'stream_enabled': is_max_per_user_per_stream_enabled, + 'max_per_user_per_stream': max_per_user_per_stream, + 'is_global_cooldown_enabled': is_global_cooldown_enabled, + 'global_cooldown_seconds': global_cooldown_seconds, + 'should_redemptions_skip_' + 'request_queue': should_redemptions_skip_request_queue, + } + ) + + req = await self.post( + '/channel_points/custom_rewards', + headers={'Authorization': f'Bearer {access_token}'}, + json=data, + ) + + match req.status_code: + case st.OK: + return s.CustomRewards.model_validate(await req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def delete_custom_reward( + self, access_token: str, broadcaster_id: int, reward_id: str + ) -> bool: + req = await self.delete( + '/channel_points/custom_rewards', + headers={'Authorization': f'Bearer {access_token}'}, + params={'broadcaster_id': broadcaster_id, 'id': reward_id}, + ) + + 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, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_custom_rewards( + self, + access_token: str, + broadcaster_id: int, + *, + reward_id: str | None = None, + only_manageable_rewards: bool = False, + ) -> s.CustomRewards: + req = await self.get( + '/channel_points/custom_rewards', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'id': reward_id, + 'only_manageable_rewards': only_manageable_rewards, + } + ), + ) + + match req.status_code: + case st.OK: + return s.CustomRewards.model_validate(await req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.NOT_FOUND + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_custom_reward_redemption( + self, + access_token: str, + broadcaster_id: int, + reward_id: str, + status: Literal['CANCELED', 'FULFILLED', 'UNFULFILLED'], + *, + redemption_id: int | list[int] | None = None, + sort: Literal['OLDEST', 'NEWEST'] = 'OLDEST', + after: str | None = None, + first: int = 20, + ) -> s.CustomRewardRedemptions: + req = await self.get( + '/channel_points/custom_rewards/redemptions', + headers={'Authorization': f'Bearer {access_token}'}, + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'reward_id': reward_id, + 'status': status, + 'redemption_id': redemption_id, + 'sort': sort, + 'after': after, + 'first': first, + } + ), + ) + + match req.status_code: + case st.OK: + return s.CustomRewardRedemptions.model_validate( + await req.json() + ) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.NOT_FOUND + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def update_custom_reward( + self, + access_token: str, + broadcaster_id: int, + reward_id: str, + *, + title: str | None = None, + prompt: str | None = None, + cost: int | None = None, + background_color: str | None = None, + is_enabled: bool | None = None, + is_user_input_required: bool | None = None, + is_max_per_stream_enabled: bool | None = None, + max_per_stream: int | None = None, + is_max_per_user_per_stream_enabled: bool | None = None, + max_per_user_per_stream: int | None = None, + is_global_cooldown_enabled: bool | None = None, + global_cooldown_seconds: int | None = None, + is_paused: bool | None = None, + should_redemptions_skip_request_queue: bool | None = None, + ) -> s.CustomRewards: + data = self.clean_dict( + { + 'title': title, + 'prompt': prompt, + 'cost': cost, + 'background_color': background_color, + 'is_enabled': is_enabled, + 'is_user_input_required': is_user_input_required, + 'is_max_per_stream_enabled': is_max_per_stream_enabled, + 'max_per_stream': max_per_stream, + 'is_max_per_user_per_' + 'stream_enabled': is_max_per_user_per_stream_enabled, + 'max_per_user_per_stream': max_per_user_per_stream, + 'is_global_cooldown_enabled': is_global_cooldown_enabled, + 'global_cooldown_seconds': global_cooldown_seconds, + 'is_paused': is_paused, + 'should_redemptions_skip_' + 'request_queue': should_redemptions_skip_request_queue, + } + ) + req = await self.patch( + '/channel_points/custom_rewards', + headers={'Authorization': f'Bearer {access_token}'}, + params={'broadcaster_id': broadcaster_id, 'id': reward_id}, + json=data, + ) + + match req.status_code: + case st.OK: + return s.CustomRewards.model_validate(await req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.NOT_FOUND + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def update_redemption_status( + self, + access_token: str, + broadcaster_id: int, + reward_id: str, + redemption_id: str | list[str], + status: Literal['CANCELED', 'FULFILLED'], + ) -> bool: + req = await self.post( + '/channel_points/custom_rewards/redemptions', + headers={'Authorization': f'Bearer {access_token}'}, + params={ + 'broadcaster_id': broadcaster_id, + 'reward_id': reward_id, + 'id': redemption_id, + }, + json={'status': status}, + ) + + 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, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_charity_campaign( + self, access_token: str, broadcaster_id: int + ) -> s.CharityCampaign: + req = await self.get( + '/charity/campaigns', + params={'broadcaster_id': broadcaster_id}, + headers={'Authorization': f'Bearer {access_token}'}, + ) + + match req.status_code: + case st.OK: + return s.CharityCampaign.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_charity_campaign_donations( + self, + access_token: str, + broadcaster_id: int, + *, + first: int = 20, + after: str | None = None, + cache_time: int | None = None, + ) -> s.CharityDonations: + req = await self.get( + '/charity/donations', + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'first': first, + 'after': after, + } + ), + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + ) + + match req.status_code: + case st.OK: + return s.CharityDonations.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_chatters( + self, + access_token: str, + broadcaster_id: int, + moderator_id: int, + *, + first: int = 20, + after: str | None = None, + cache_time: int | None = None, + ) -> s.Chatters: + req = await self.get( + '/chat/chatters', + params=self.clean_dict( + { + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + 'first': first, + 'after': after, + } + ), + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + ) + + match req.status_code: + case st.OK: + return s.Chatters.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_channel_emotes( + self, + access_token: str, + broadcaster_id: int, + *, + cache_time: int | None = None, + ) -> s.ChannelEmotes: + req = await self.get( + '/chat/emotes', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.ChannelEmotes.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_global_emotes( + self, access_token: str, *, cache_time: int | None = None + ) -> s.GlobalEmotes: + req = await self.get( + '/chat/emotes/global', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + ) + + match req.status_code: + case st.OK: + return s.GlobalEmotes.model_validate(await req.json()) + + case st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + 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, + ) -> s.EmoteSets: + req = await self.get( + '/chat/emotes/set', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params={'emote_set_id': emote_set_id}, + ) + + match req.status_code: + case st.OK: + return s.EmoteSets.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_channel_chat_badges( + self, + access_token: str, + broadcaster_id: int, + *, + cache_time: int | None = None, + ) -> s.ChannelChatBadges: + req = await self.get( + '/chat/badges', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.ChannelChatBadges.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_global_chat_badges( + self, access_token: str, *, cache_time: int | None = None + ) -> s.GlobalChatBadges: + req = await self.get( + '/chat/badges/global', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + ) + + match req.status_code: + case st.OK: + return s.GlobalChatBadges.model_validate(await req.json()) + + case st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_chat_settings( + self, + access_token: str, + broadcaster_id: int, + *, + moderator_id: int | None = None, + cache_time: int | None = None, + ) -> s.ChatSettings: + req = await self.get( + '/chat/settings', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_shared_chat_session( + self, + access_token: str, + broadcaster_id: int, + *, + cache_time: int | None = None, + ) -> s.SharedChatSession: + req = await self.get( + '/shared_chat/session', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.SharedChatSession.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_user_emotes( + self, + access_token: str, + user_id: int, + *, + after: str | None = None, + broadcaster_id: int | None = None, + cache_time: int | None = None, + ) -> s.UserEmotes: + req = await self.get( + '/chat/emotes/global', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def update_chat_settings( + self, + access_token: str, + broadcaster_id: int, + moderator_id: int, + *, + 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, + ) -> s.ChatSettings: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def send_chat_announcement( + self, + access_token: str, + broadcaster_id: int, + moderator_id: int, + message: str, + color: Literal[ + 'blue', 'green', 'orange', 'purple', 'primary' + ] = 'primary', + ) -> bool: + 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.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def send_shoutout( + self, + access_token: str, + from_broadcaster_id: int, + to_broadcaster_id: int, + moderator_id: int, + ) -> bool: + 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.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def send_chat_message( + self, + access_token: str, + broadcaster_id: int, + sender_id: int, + message: str, + *, + reply_parent_message_id: str | None = None, + for_source_only: bool | None = None, + ) -> s.Message: + 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(await req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_user_chat_color( + self, + access_token: str, + user_id: int | list[int], + *, + cache_time: int | None = None, + ) -> s.UserChatColor: + req = await self.get( + '/chat/colors', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params={'user_id': user_id}, + ) + + match req.status_code: + case st.OK: + return s.UserChatColor.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def update_user_chat_color( + self, + access_token: str, + user_id: int, + color: str, + ) -> bool: + 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.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def create_clip( + self, + access_token: str, + broadcaster_id: int, + *, + has_delay: bool | None = None, + ) -> bool: + 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.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def get_clips( + self, + access_token: str, + *, + 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, + ) -> s.Clips: + req = await self.get( + '/clips', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + 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, + ) -> s.ClipsDownloads: + req = await self.get( + '/clips/downloads', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.INTERNAL_SERVER_ERROR + ): + raise s.ClientError( + req.status_code, (await 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 + ) -> s.Conduits: + req = await self.get( + '/eventsub/conduits', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + ) + + match req.status_code: + case st.OK: + return s.Conduits.model_validate(await req.json()) + + case st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def create_conduits( + self, access_token: str, shard_count: int + ) -> s.Conduits: + 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(await req.json()) + + case st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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 + ) -> s.Conduits: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def delete_conduit(self, access_token: str, conduit_id: str) -> bool: + 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, (await 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, + ) -> s.ConduitShards: + req = await self.get( + '/eventsub/conduits/shards', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await 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], + ) -> s.ConduitShards: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await 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', + ) -> s.ContentClassificationLabels: + 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( + await req.json() + ) + + case st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.DropsEntitlements: + req = await self.get( + '/entitlements/drops', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.INTERNAL_SERVER_ERROR + ): + raise s.ClientError( + req.status_code, (await 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, + ) -> s.UpdateDropsEntitlements: + 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( + await req.json() + ) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.INTERNAL_SERVER_ERROR: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.ExtensionConfigurationSegment: + req = await self.get( + '/extensions/configurations', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {jwt_token}', + 'X-Cache-TTL': str(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( + await req.json() + ) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: + raise s.ClientError( + req.status_code, (await 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, + ) -> bool: + 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, (await 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, + ) -> bool: + 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, (await 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, + ) -> bool: + 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, (await 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, + ) -> s.ExtensionLiveChannels: + req = await self.get( + '/extensions/live', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.ExtensionSecrets: + req = await self.get( + '/extensions/jwt/secrets', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {jwt_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params={'extension_id': extension_id}, + ) + + match req.status_code: + case st.OK: + return s.ExtensionSecrets.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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 + ) -> s.ExtensionSecrets: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> bool: + 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, (await 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, + ) -> s.Extensions: + req = await self.get( + '/extensions', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {jwt_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.Extensions: + req = await self.get( + '/extensions/released', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.ExtensionBitsProducts: + req = await self.get( + '/bits/extensions', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'should_include_all': should_include_all, + } + ), + ) + + match req.status_code: + case st.OK: + return s.ExtensionBitsProducts.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.ExtensionBitsProducts: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.EventsubBaseSubscriptions: + 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.EventsubBaseSubscriptions.model_validate( + await req.json() + ) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.CONFLICT + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError( + req.status_code, (await 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 + ) -> None: + 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 None + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.EventsubBaseSubscriptions: + req = await self.get( + '/eventsub/subscriptions', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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.EventsubBaseSubscriptions.model_validate( + await req.json() + ) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.Games: + req = await self.get( + '/games/top', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + { + 'first': first, + 'after': after, + 'before': before, + } + ), + ) + + match req.status_code: + case st.OK: + return s.Games.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.Games: + req = await self.get( + '/games', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.CreatorGoals: + req = await self.get( + '/creator_goals', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.CreatorGoals.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.HypeTrainStatus: + req = await self.get( + '/hypetrain/status', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.HypeTrainStatus.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.INTERNAL_SERVER_ERROR: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.AutomodStatus: + req = await self.get( + '/moderation/enforcements/status', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.AutomodStatus.model_validate(await req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError( + req.status_code, (await 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'], + ) -> bool: + 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, (await 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, + ) -> s.AutomodSettings: + req = await self.get( + '/moderation/automod/settings', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params={ + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + }, + ) + + match req.status_code: + case st.OK: + return s.AutomodSettings.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.AutomodSettings: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.BannedUsers: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.BanUser: + 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(await req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.CONFLICT + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError( + req.status_code, (await 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, + ) -> None: + 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, (await 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, + ) -> s.UnbanRequests: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.UnbanRequests: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.BlockedTerms: + req = await self.get( + '/moderation/blocked_terms', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.BlockedTerms: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError( + req.status_code, (await 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, + ) -> bool: + 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, (await 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, + ) -> bool: + 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, (await 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, + ) -> s.ModeratedChannels: + req = await self.get( + '/moderation/channels', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.Moderators: + req = await self.get( + '/moderation/moderators', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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 + ) -> bool: + 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, (await 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 + ) -> bool: + 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, (await 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, + ) -> s.VIPs: + req = await self.get( + '/channels/vips', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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 + ) -> bool: + 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, (await 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 + ) -> bool: + 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, (await 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, + ) -> s.ShieldModeStatus: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.ShieldModeStatus: + req = await self.get( + '/moderation/shield_mode', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params={ + 'broadcaster_id': broadcaster_id, + 'moderator_id': moderator_id, + }, + ) + + match req.status_code: + case st.OK: + return s.ShieldModeStatus.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.UserWarns: + 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(await 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, (await 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, + ) -> s.Polls: + req = await self.get( + '/polls', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.Polls: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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'], + ) -> s.Polls: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.Predictions: + req = await self.get( + '/predictions', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.Predictions: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.Predictions: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.Raids: + 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(await req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.NOT_FOUND + | st.CONFLICT + | st.TOO_MANY_REQUESTS + ): + raise s.ClientError( + req.status_code, (await 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 + ) -> bool: + 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, (await 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, + ) -> s.Schedules: + req = await self.get( + '/schedule', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case ( + st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN | st.NOT_FOUND + ): + raise s.ClientError( + req.status_code, (await 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, + ) -> bytes: + req = await self.get( + '/schedule/icalendar', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return await req.json() + + case st.BAD_REQUEST: + raise s.ClientError( + req.status_code, (await 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, + ) -> bool: + 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, (await 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, + ) -> s.Schedules: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.Schedules: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await 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 + ) -> bool: + 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, (await 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, + ) -> s.Categories: + req = await self.get( + '/search/categories', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict( + {'query': query, 'first': first, 'after': after} + ), + ) + + match req.status_code: + case st.OK: + return s.Categories.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.Channels: + req = await self.get( + '/search/channels', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.StreamKeys: + req = await self.get( + '/streams/key', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.StreamKeys.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.Streams: + req = await self.get( + '/streams', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.Streams: + req = await self.get( + '/streams/followed', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.CreateStreamMarkers: + 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(await req.json()) + + case ( + st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN | st.NOT_FOUND + ): + raise s.ClientError( + req.status_code, (await 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, + ) -> s.StreamMarkers: + req = await self.get( + '/streams/markers', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case ( + st.BAD_REQUEST | st.UNAUTHORIZED | st.FORBIDDEN | st.NOT_FOUND + ): + raise s.ClientError( + req.status_code, (await 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, + ) -> s.BroadcasterSubscriptions: + req = await self.get( + '/subscriptions', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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( + await req.json() + ) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.UserSubscription: + req = await self.get( + '/subscriptions/user', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.ChannelTeams: + req = await self.get( + '/teams/channel', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params={'broadcaster_id': broadcaster_id}, + ) + + match req.status_code: + case st.OK: + return s.ChannelTeams.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.Teams: + req = await self.get( + '/teams', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict({'name': name, 'id': team_id}), + ) + + match req.status_code: + case st.OK: + return s.Teams.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.Users: + req = await self.get( + '/users', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params=self.clean_dict({'id': user_id, 'login': login}), + ) + + match req.status_code: + case st.OK: + return s.Users.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') + + async def update_user( + self, access_token: str, *, description: str = '' + ) -> s.Users: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.TOO_MANY_REQUESTS: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.AuthorizationsByUser: + req = await self.get( + '/authorization/users', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params={'user_id': user_id}, + ) + + match req.status_code: + case st.OK: + return s.AuthorizationsByUser.model_validate(await req.json()) + + case ( + st.BAD_REQUEST + | st.UNAUTHORIZED + | st.FORBIDDEN + | st.INTERNAL_SERVER_ERROR + ): + raise s.ClientError( + req.status_code, (await 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, + ) -> s.UserBlockList: + req = await self.get( + '/users/blocks', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> bool: + 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, (await 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 + ) -> bool: + 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, (await 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 + ) -> s.UserExtensions: + req = await self.get( + '/users/extensions/list', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + ) + + match req.status_code: + case st.OK: + return s.UserExtensions.model_validate(await req.json()) + + case st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> s.UserActiveExtensions: + req = await self.get( + '/users/extensions', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(cache_time), + } + ), + params={'user_id': user_id}, + ) + + match req.status_code: + case st.OK: + return s.UserActiveExtensions.model_validate(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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 + ) -> bool: + 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, (await 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, + ) -> s.Videos: + req = await self.get( + '/videos', + headers=self.clean_dict( + { + 'Authorization': f'Bearer {access_token}', + 'X-Cache-TTL': str(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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED | st.NOT_FOUND: + raise s.ClientError( + req.status_code, (await 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] + ) -> s.DeleteVideos: + 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(await req.json()) + + case st.BAD_REQUEST | st.UNAUTHORIZED: + raise s.ClientError( + req.status_code, (await 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, + ) -> bool: + 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, (await req.json())['message'] + ) + + case _: + raise s.InternalError(req.status_code, 'Internal Server Error') diff --git a/src/oxidetwitch/auth.py b/src/oxidetwitch/auth.py new file mode 100644 index 0000000..2c99fd2 --- /dev/null +++ b/src/oxidetwitch/auth.py @@ -0,0 +1,131 @@ +from urllib.parse import urlencode + +from oxidehttp import status as st +from oxidehttp.client import OxideHTTP + +from . import schema as s +from . import scopes + + +class TwitchAuthClient(OxideHTTP): + def __init__( + self, + client_id: str, + client_secret: str, + redirect_uri: str, + redis_url: str | None = None, + proxy_url: str | None = None, + ) -> None: + 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, + ratelimit_key='twitch' if redis_url else None, + ratelimit_limit=700 if redis_url else None, + proxy_url=proxy_url, + ) + + async def create_authorization_code_grant_flow_url( + self, + scope: list[scopes.Any], + *, + force_verify: bool = False, + state: str | None = None, + ) -> str: + 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) -> s.AppAccessToken: + 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) -> s.UserAccessToken | None: + 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 + ) -> s.AccessTokenValidation | None: + 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 + ) -> s.UserAccessToken | None: + 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/oxidetwitch/eventsub/__init__.py b/src/oxidetwitch/eventsub/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/oxidetwitch/eventsub/conditions.py b/src/oxidetwitch/eventsub/conditions.py new file mode 100644 index 0000000..4df29e0 --- /dev/null +++ b/src/oxidetwitch/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/oxidetwitch/eventsub/statuses.py b/src/oxidetwitch/eventsub/statuses.py new file mode 100644 index 0000000..2839ccf --- /dev/null +++ b/src/oxidetwitch/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/oxidetwitch/eventsub/subscriptions.py b/src/oxidetwitch/eventsub/subscriptions.py new file mode 100644 index 0000000..ca8e803 --- /dev/null +++ b/src/oxidetwitch/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/oxidetwitch/eventsub/transports.py b/src/oxidetwitch/eventsub/transports.py new file mode 100644 index 0000000..fefffb3 --- /dev/null +++ b/src/oxidetwitch/eventsub/transports.py @@ -0,0 +1,30 @@ +from datetime import datetime +from typing import Literal + +from pydantic import BaseModel + + +class BaseWebhookTransport(BaseModel): + method: Literal['webhook'] + callback: str + + +class BaseWebsocketConnectedTransport(BaseModel): + method: Literal['websocket'] + session_id: str + connected_at: datetime + + +class BaseWebsocketDisconnectedTransport(BaseWebsocketConnectedTransport): + disconnected_at: datetime + + +class SubscribeWebhookTransport(BaseWebhookTransport): + secret: str + + +Any = ( + BaseWebhookTransport + | BaseWebsocketConnectedTransport + | BaseWebsocketDisconnectedTransport +) diff --git a/src/oxidetwitch/eventsub/types.py b/src/oxidetwitch/eventsub/types.py new file mode 100644 index 0000000..d71899c --- /dev/null +++ b/src/oxidetwitch/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/oxidetwitch/py.typed b/src/oxidetwitch/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/oxidetwitch/schema.py b/src/oxidetwitch/schema.py new file mode 100644 index 0000000..2312b39 --- /dev/null +++ b/src/oxidetwitch/schema.py @@ -0,0 +1,1487 @@ +from datetime import datetime +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 + error: str + + def __init__(self, status_code: int, error: str) -> None: + self.status_code = status_code + self.error = error + super().__init__(f'{status_code}: {error}') + + +class ClientError(Error): + pass + + +class InternalError(Error): + pass + + +class Pagination(BaseSchema): + cursor: str + + +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(BaseSchema): + data: list[StartCommercialData] + + +class AdScheduleData(BaseSchema): + next_ad_at: datetime | None + last_ad_at: datetime | None + duration: int + preroll_free_time: int + snooze_count: int + snooze_refresh_at: datetime + + +class AdSchedule(BaseSchema): + data: list[AdScheduleData] + + +class SnoozeNextAdData(BaseSchema): + snooze_count: int + snooze_refresh_at: datetime + next_ad_at: datetime + + +class SnoozeNextAd(BaseSchema): + data: list[SnoozeNextAdData] + + +class DateRange(BaseSchema): + started_at: datetime + ended_at: datetime + + +class ExtensionAnalyticsData(BaseSchema): + extension_id: str + URL: str + type: str + date_range: DateRange + + +class ExtensionAnalytics(BaseSchema): + data: list[ExtensionAnalyticsData] + pagination: Pagination | dict[Any, Any] | None = None + + +class GameAnalyticsData(BaseSchema): + game_id: int + URL: str + type: str + date_range: DateRange + + +class GameAnalytics(BaseSchema): + data: list[GameAnalyticsData] + pagination: Pagination | dict[Any, Any] | None = None + + +class BitsLeaderboardData(BaseSchema): + user_id: int + user_login: str + user_name: str + rank: int + score: int + + +class BitsLeaderboard(BaseSchema): + data: list[BitsLeaderboardData] + date_range: DateRange + total: int + + +class CheermotesImageAnimated(BaseSchema): + field_1: str = Field(..., alias='1') + field_1_5: str = Field(..., alias='1.5') + field_2: str = Field(..., alias='2') + field_3: str = Field(..., alias='3') + field_4: str = Field(..., alias='4') + + +class CheermotesImageStatic(BaseSchema): + field_1: str = Field(..., alias='1') + field_1_5: str = Field(..., alias='1.5') + field_2: str = Field(..., alias='2') + field_3: str = Field(..., alias='3') + field_4: str = Field(..., alias='4') + + +class CheermotesImage(BaseSchema): + animated: CheermotesImageAnimated + static: CheermotesImageStatic + + +class CheermotesImages(BaseSchema): + light: CheermotesImage + dark: CheermotesImage + + +class CheermotesTier(BaseSchema): + min_bits: int + id: Literal['1', '100', '500', '1000', '5000', '10000', '100000'] + color: str + can_cheer: bool + show_in_bits_card: bool + images: CheermotesImages + + +class CheermotesData(BaseSchema): + prefix: str + tiers: list[CheermotesTier] + type: Literal[ + 'global_first_party', + 'global_third_party', + 'channel_custom', + 'display_only', + 'sponsored', + ] + order: int + last_updated: datetime + is_charitable: bool + + +class Cheermotes(BaseSchema): + data: list[CheermotesData] + + +class ExtensionProductCost(BaseSchema): + amount: int + type: Literal['bits'] + + +class ExtensionProductData(BaseSchema): + domain: str + sku: str + cost: ExtensionProductCost + inDevelopment: bool + displayName: str + expiration: str + broadcast: bool + + +class ExtensionTransactionsData(BaseSchema): + id: str + timestamp: datetime + broadcaster_id: int + broadcaster_login: str + broadcaster_name: str + user_id: int + user_login: str + user_name: str + product_type: Literal['BITS_IN_EXTENSION'] + product_data: ExtensionProductData + + +class ExtensionTransactions(BaseSchema): + data: list[ExtensionTransactionsData] + pagination: Pagination | dict[Any, Any] | None = None + + +class ContentClassificationLabel(TypedDict): + id: Literal[ + 'DebatedSocialIssuesAndPolitics', + 'DrugsIntoxication', + 'SexualThemes', + 'ViolentGraphic', + 'Gambling', + 'ProfanityVulgarity', + ] + is_enabled: bool + + +class ChannelInformation(BaseSchema): + broadcaster_id: int + broadcaster_login: str + broadcaster_name: str + broadcaster_language: str + game_name: str + game_id: int | str + title: str + delay: int + tags: list[str] + content_classification_labels: list[str] + is_branded_content: bool + + +class ChannelsInformation(BaseSchema): + data: list[ChannelInformation] + + +class ChannelEditor(BaseSchema): + user_id: int + user_name: str + created_at: datetime + + +class ChannelEditors(BaseSchema): + data: list[ChannelEditor] + + +class FollowedChannel(BaseSchema): + broadcaster_id: int + broadcaster_login: str + broadcaster_name: str + followed_at: datetime + + +class FollowedChannels(BaseSchema): + data: list[FollowedChannel] + pagination: Pagination | dict[Any, Any] | None = None + total: int + + +class ChannelFollower(BaseSchema): + user_id: int + user_login: str + user_name: str + followed_at: datetime + + +class ChannelFollowers(BaseSchema): + data: list[ChannelFollower] + pagination: Pagination | dict[Any, Any] | None = None + total: int + + +class CustomRewardImage(BaseSchema): + url_1x: str + url_2x: str + url_4x: str + + +class MaxPerStreamSetting(BaseSchema): + is_enabled: bool + max_per_stream: int + + +class MaxPerUserPerStreamSetting(BaseSchema): + is_enabled: bool + max_per_user_per_stream: int + + +class GlobalCooldownSetting(BaseSchema): + is_enabled: bool + global_cooldown_seconds: int + + +class CustomReward(BaseSchema): + broadcaster_id: int + broadcaster_login: str + broadcaster_name: str + id: str + title: str + prompt: str + cost: int + is_paused: bool + is_in_stock: bool + background_color: str + is_enabled: bool + is_user_input_required: bool + should_redemptions_skip_request_queue: bool + redemptions_redeemed_current_stream: int | None + cooldown_expires_at: datetime | None + image: CustomRewardImage | None + default_image: CustomRewardImage + max_per_stream_setting: MaxPerStreamSetting + max_per_user_per_stream_setting: MaxPerUserPerStreamSetting + global_cooldown_setting: GlobalCooldownSetting + + +class CustomRewards(BaseSchema): + data: list[CustomReward] + + +class CustomRewardRedemptionReward(BaseSchema): + id: str + title: str + prompt: str + cost: int + + +class CustomRewardRedemption(BaseSchema): + id: int + broadcaster_id: int + broadcaster_login: str + broadcaster_name: str + user_id: int + user_login: str + user_name: str + user_input: str + status: Literal['CANCELED', 'FULFILLED', 'UNFULFILLED'] + redeemed_at: datetime + reward: CustomRewardRedemptionReward + + +class CustomRewardRedemptions(BaseSchema): + data: list[CustomRewardRedemption] + + +class CharityCampaignCurrentAmount(BaseSchema): + amount: int + decimal_places: int + currency: str + + +class CharityCampaignTargetAmount(BaseSchema): + value: int + decimal_places: int + currency: str + + +class CharityCampaignData(BaseSchema): + id: str + broadcaster_id: int + broadcaster_login: str + broadcaster_name: str + charity_name: str + charity_description: str + charity_logo: str + charity_website: str + current_amount: CharityCampaignCurrentAmount + target_amount: CharityCampaignTargetAmount + + +class CharityCampaign(BaseSchema): + data: list[CharityCampaignData] + + +class CharityDonationAmount(BaseSchema): + value: int + decimal_places: int + currency: str + + +class CharityDonation(BaseSchema): + id: str + campaign_id: str + user_id: int + user_login: str + user_name: str + amount: CharityDonationAmount + + +class CharityDonations(BaseSchema): + data: list[CharityDonation] + pagination: Pagination | dict[Any, Any] | None = None + + +class ChattersData(BaseSchema): + user_id: int + user_login: str + user_name: str + + +class Chatters(BaseSchema): + data: list[ChattersData] + pagination: Pagination | dict[Any, Any] | None = None + total: int + + +class ChannelEmoteImages(BaseSchema): + url_1x: str + url_2x: str + url_4x: str + + +class ChannelEmote(BaseSchema): + id: int + name: str + images: ChannelEmoteImages + tier: int + 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']] + + +class ChannelEmotes(BaseSchema): + data: list[ChannelEmote] + template: str + + +class GlobalEmotes(ChannelEmotes): + pass + + +class EmoteSets(ChannelEmotes): + pass + + +class ChannelChatBadgesVersion(BaseSchema): + 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(BaseSchema): + set_id: str + versions: list[ChannelChatBadgesVersion] + + +class ChannelChatBadges(BaseSchema): + data: list[ChannelChatBadgesData] + + +class GlobalChatBadges(ChannelChatBadges): + pass + + +class ChatSettingsData(BaseSchema): + 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(BaseSchema): + data: list[ChatSettingsData] + + +class SharedChatSessionParticipant(BaseSchema): + broadcaster_id: int + + +class SharedChatSessionData(BaseSchema): + session_id: str + host_broadcaster_id: int + participants: list[SharedChatSessionParticipant] + created_at: datetime + updated_at: datetime + + +class SharedChatSession(BaseSchema): + data: list[SharedChatSessionData] + + +class UserEmotes(ChannelEmotes): + pagination: Pagination | dict[Any, Any] | None = None + + +class MessageDropReason(BaseSchema): + code: str + message: str + + +class MessageData(BaseSchema): + message_id: str + is_sent: bool + drop_reason: MessageDropReason | None = None + + +class Message(BaseSchema): + data: list[MessageData] + + +class UserChatColorData(BaseSchema): + user_id: int + user_login: str + user_name: str + color: str + + +class UserChatColor(BaseSchema): + data: list[UserChatColorData] + + +class Clip(BaseSchema): + 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(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 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] + tag_ids: list[str] = Field(..., deprecated=True) + 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 + view_count: int = Field(..., deprecated=True) + + +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/oxidetwitch/scopes.py b/src/oxidetwitch/scopes.py new file mode 100644 index 0000000..403ef09 --- /dev/null +++ b/src/oxidetwitch/scopes.py @@ -0,0 +1,166 @@ +from typing import Literal + +# Analytics +ANALYTICS_READ_EXTENSIONS = 'analytics:read:extensions' +ANALYTICS_READ_GAMES = 'analytics:read:games' + +# Bits +BITS_READ = 'bits:read' + +# Channel +CHANNEL_MANAGE_ADS = 'channel:manage:ads' +CHANNEL_READ_ADS = 'channel:read:ads' +CHANNEL_MANAGE_BROADCAST = 'channel:manage:broadcast' +CHANNEL_READ_CHARITY = 'channel:read:charity' +CHANNEL_EDIT_COMMERCIAL = 'channel:edit:commercial' +CHANNEL_READ_EDITORS = 'channel:read:editors' +CHANNEL_MANAGE_EXTENSIONS = 'channel:manage:extensions' +CHANNEL_READ_GOALS = 'channel:read:goals' +CHANNEL_READ_GUEST_STAR = 'channel:read:guest_star' +CHANNEL_MANAGE_GUEST_STAR = 'channel:manage:guest_star' +CHANNEL_READ_HYPE_TRAIN = 'channel:read:hype_train' +CHANNEL_MANAGE_MODERATORS = 'channel:manage:moderators' +CHANNEL_READ_POLLS = 'channel:read:polls' +CHANNEL_MANAGE_POLLS = 'channel:manage:polls' +CHANNEL_READ_PREDICTIONS = 'channel:read:predictions' +CHANNEL_MANAGE_PREDICTIONS = 'channel:manage:predictions' +CHANNEL_MANAGE_RAIDS = 'channel:manage:raids' +CHANNEL_READ_REDEMPTIONS = 'channel:read:redemptions' +CHANNEL_MANAGE_REDEMPTIONS = 'channel:manage:redemptions' +CHANNEL_MANAGE_SCHEDULE = 'channel:manage:schedule' +CHANNEL_READ_STREAM_KEY = 'channel:read:stream_key' +CHANNEL_READ_SUBSCRIPTIONS = 'channel:read:subscriptions' +CHANNEL_MANAGE_VIDEOS = 'channel:manage:videos' +CHANNEL_READ_VIPS = 'channel:read:vips' +CHANNEL_MANAGE_VIPS = 'channel:manage:vips' +CHANNEL_BOT = 'channel:bot' +CHANNEL_MODERATE = 'channel:moderate' + +# clips +CLIPS_EDIT = 'clips:edit' + +# Moderation +MODERATION_READ = 'moderation:read' +MODERATOR_MANAGE_ANNOUNCEMENTS = 'moderator:manage:announcements' +MODERATOR_MANAGE_AUTOMOD = 'moderator:manage:automod' +MODERATOR_READ_AUTOMOD_SETTINGS = 'moderator:read:automod_settings' +MODERATOR_MANAGE_AUTOMOD_SETTINGS = 'moderator:manage:automod_settings' +MODERATOR_MANAGE_BANNED_USERS = 'moderator:manage:banned_users' +MODERATOR_READ_BLOCKED_TERMS = 'moderator:read:blocked_terms' +MODERATOR_MANAGE_BLOCKED_TERMS = 'moderator:manage:blocked_terms' +MODERATOR_MANAGE_CHAT_MESSAGES = 'moderator:manage:chat_messages' +MODERATOR_READ_CHAT_SETTINGS = 'moderator:read:chat_settings' +MODERATOR_MANAGE_CHAT_SETTINGS = 'moderator:manage:chat_settings' +MODERATOR_READ_CHATTERS = 'moderator:read:chatters' +MODERATOR_READ_FOLLOWERS = 'moderator:read:followers' +MODERATOR_READ_GUEST_STAR = 'moderator:read:guest_star' +MODERATOR_MANAGE_GUEST_STAR = 'moderator:manage:guest_star' +MODERATOR_READ_SHIELD_MODE = 'moderator:read:shield_mode' +MODERATOR_MANAGE_SHIELD_MODE = 'moderator:manage:shield_mode' +MODERATOR_READ_SHOUTOUTS = 'moderator:read:shoutouts' +MODERATOR_MANAGE_SHOUTOUTS = 'moderator:manage:shoutouts' +MODERATOR_READ_UNBAN_REQUESTS = 'moderator:read:unban_requests' +MODERATOR_MANAGE_UNBAN_REQUESTS = 'moderator:manage:unban_requests' +MODERATOR_READ_WARNINGS = 'moderator:read:warnings' +MODERATOR_MANAGE_WARNINGS = 'moderator:manage:warnings' + +# User +USER_EDIT = 'user:edit' +USER_EDIT_FOLLOWS = 'user:edit:follows' +USER_READ_BLOCKED_USERS = 'user:read:blocked_users' +USER_MANAGE_BLOCKED_USERS = 'user:manage:blocked_users' +USER_READ_BROADCAST = 'user:read:broadcast' +USER_MANAGE_CHAT_COLOR = 'user:manage:chat_color' +USER_READ_EMAIL = 'user:read:email' +USER_READ_EMOTES = 'user:read:emotes' +USER_READ_FOLLOWS = 'user:read:follows' +USER_READ_MODERATED_CHANNELS = 'user:read:moderated_channels' +USER_READ_SUBSCRIPTIONS = 'user:read:subscriptions' +USER_MANAGE_WHISPERS = 'user:manage:whispers' +USER_BOT = 'user:bot' +USER_READ_CHAT = 'user:read:chat' +USER_WRITE_CHAT = 'user:write:chat' + +# Chat +CHAT_EDIT = 'chat:edit' +CHAT_READ = 'chat:read' + +# Whispers +WHISPERS_READ = 'whispers:read' +WHISPERS_EDIT = 'whispers:edit' + +type Any = Literal[ + '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', +]