vox-sdk 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. vox_sdk-0.1.0/PKG-INFO +16 -0
  2. vox_sdk-0.1.0/README.md +106 -0
  3. vox_sdk-0.1.0/pyproject.toml +38 -0
  4. vox_sdk-0.1.0/setup.cfg +4 -0
  5. vox_sdk-0.1.0/setup.py +43 -0
  6. vox_sdk-0.1.0/src/vox_sdk/__init__.py +15 -0
  7. vox_sdk-0.1.0/src/vox_sdk/_media.py +6 -0
  8. vox_sdk-0.1.0/src/vox_sdk/api/__init__.py +1 -0
  9. vox_sdk-0.1.0/src/vox_sdk/api/auth.py +153 -0
  10. vox_sdk-0.1.0/src/vox_sdk/api/bots.py +63 -0
  11. vox_sdk-0.1.0/src/vox_sdk/api/channels.py +196 -0
  12. vox_sdk-0.1.0/src/vox_sdk/api/dms.py +102 -0
  13. vox_sdk-0.1.0/src/vox_sdk/api/e2ee.py +85 -0
  14. vox_sdk-0.1.0/src/vox_sdk/api/embeds.py +19 -0
  15. vox_sdk-0.1.0/src/vox_sdk/api/emoji.py +66 -0
  16. vox_sdk-0.1.0/src/vox_sdk/api/federation.py +84 -0
  17. vox_sdk-0.1.0/src/vox_sdk/api/files.py +49 -0
  18. vox_sdk-0.1.0/src/vox_sdk/api/invites.py +47 -0
  19. vox_sdk-0.1.0/src/vox_sdk/api/members.py +61 -0
  20. vox_sdk-0.1.0/src/vox_sdk/api/messages.py +125 -0
  21. vox_sdk-0.1.0/src/vox_sdk/api/moderation.py +78 -0
  22. vox_sdk-0.1.0/src/vox_sdk/api/roles.py +106 -0
  23. vox_sdk-0.1.0/src/vox_sdk/api/search.py +19 -0
  24. vox_sdk-0.1.0/src/vox_sdk/api/server.py +52 -0
  25. vox_sdk-0.1.0/src/vox_sdk/api/sync.py +33 -0
  26. vox_sdk-0.1.0/src/vox_sdk/api/users.py +90 -0
  27. vox_sdk-0.1.0/src/vox_sdk/api/voice.py +104 -0
  28. vox_sdk-0.1.0/src/vox_sdk/api/webhooks.py +59 -0
  29. vox_sdk-0.1.0/src/vox_sdk/client.py +225 -0
  30. vox_sdk-0.1.0/src/vox_sdk/errors.py +80 -0
  31. vox_sdk-0.1.0/src/vox_sdk/gateway.py +285 -0
  32. vox_sdk-0.1.0/src/vox_sdk/http.py +130 -0
  33. vox_sdk-0.1.0/src/vox_sdk/models/__init__.py +232 -0
  34. vox_sdk-0.1.0/src/vox_sdk/models/auth.py +64 -0
  35. vox_sdk-0.1.0/src/vox_sdk/models/base.py +7 -0
  36. vox_sdk-0.1.0/src/vox_sdk/models/bots.py +65 -0
  37. vox_sdk-0.1.0/src/vox_sdk/models/channels.py +52 -0
  38. vox_sdk-0.1.0/src/vox_sdk/models/dms.py +14 -0
  39. vox_sdk-0.1.0/src/vox_sdk/models/e2ee.py +35 -0
  40. vox_sdk-0.1.0/src/vox_sdk/models/emoji.py +25 -0
  41. vox_sdk-0.1.0/src/vox_sdk/models/enums.py +24 -0
  42. vox_sdk-0.1.0/src/vox_sdk/models/errors.py +92 -0
  43. vox_sdk-0.1.0/src/vox_sdk/models/events.py +670 -0
  44. vox_sdk-0.1.0/src/vox_sdk/models/federation.py +35 -0
  45. vox_sdk-0.1.0/src/vox_sdk/models/files.py +13 -0
  46. vox_sdk-0.1.0/src/vox_sdk/models/invites.py +22 -0
  47. vox_sdk-0.1.0/src/vox_sdk/models/members.py +26 -0
  48. vox_sdk-0.1.0/src/vox_sdk/models/messages.py +51 -0
  49. vox_sdk-0.1.0/src/vox_sdk/models/moderation.py +44 -0
  50. vox_sdk-0.1.0/src/vox_sdk/models/roles.py +14 -0
  51. vox_sdk-0.1.0/src/vox_sdk/models/server.py +55 -0
  52. vox_sdk-0.1.0/src/vox_sdk/models/sync.py +22 -0
  53. vox_sdk-0.1.0/src/vox_sdk/models/users.py +43 -0
  54. vox_sdk-0.1.0/src/vox_sdk/models/voice.py +36 -0
  55. vox_sdk-0.1.0/src/vox_sdk/pagination.py +70 -0
  56. vox_sdk-0.1.0/src/vox_sdk/permissions.py +263 -0
  57. vox_sdk-0.1.0/src/vox_sdk/rate_limit.py +113 -0
  58. vox_sdk-0.1.0/src/vox_sdk.egg-info/PKG-INFO +16 -0
  59. vox_sdk-0.1.0/src/vox_sdk.egg-info/SOURCES.txt +70 -0
  60. vox_sdk-0.1.0/src/vox_sdk.egg-info/dependency_links.txt +1 -0
  61. vox_sdk-0.1.0/src/vox_sdk.egg-info/requires.txt +13 -0
  62. vox_sdk-0.1.0/src/vox_sdk.egg-info/top_level.txt +1 -0
  63. vox_sdk-0.1.0/tests/test_api_groups.py +1484 -0
  64. vox_sdk-0.1.0/tests/test_client.py +204 -0
  65. vox_sdk-0.1.0/tests/test_errors.py +113 -0
  66. vox_sdk-0.1.0/tests/test_gateway.py +572 -0
  67. vox_sdk-0.1.0/tests/test_http.py +343 -0
  68. vox_sdk-0.1.0/tests/test_media_video.py +266 -0
  69. vox_sdk-0.1.0/tests/test_models.py +175 -0
  70. vox_sdk-0.1.0/tests/test_pagination.py +191 -0
  71. vox_sdk-0.1.0/tests/test_permissions.py +193 -0
  72. vox_sdk-0.1.0/tests/test_rate_limit.py +143 -0
vox_sdk-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: vox-sdk
3
+ Version: 0.1.0
4
+ Summary: Python client SDK for the Vox protocol
5
+ Requires-Python: >=3.11
6
+ Requires-Dist: httpx>=0.27
7
+ Requires-Dist: websockets>=13.0
8
+ Requires-Dist: pydantic>=2.0
9
+ Requires-Dist: zstandard>=0.23
10
+ Provides-Extra: dev
11
+ Requires-Dist: pytest>=8.0; extra == "dev"
12
+ Requires-Dist: pytest-asyncio>=0.24; extra == "dev"
13
+ Requires-Dist: pytest-cov>=5.0; extra == "dev"
14
+ Requires-Dist: ruff>=0.6; extra == "dev"
15
+ Provides-Extra: media
16
+ Requires-Dist: vox-media>=0.1; extra == "media"
@@ -0,0 +1,106 @@
1
+ # Vox Python SDK
2
+
3
+ Python client SDK for the Vox protocol.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install vox-sdk
9
+ ```
10
+
11
+ ## Quickstart
12
+
13
+ ```python
14
+ import asyncio
15
+ from vox_sdk import Client
16
+
17
+ async def main():
18
+ async with Client("https://vox.example.com") as client:
19
+ # Login
20
+ await client.login("alice", "password123")
21
+
22
+ # Send a message
23
+ msg = await client.messages.send(feed_id=1, body="Hello from the SDK!")
24
+
25
+ # List members
26
+ members = await client.members.list()
27
+ print(f"{len(members.items)} members online")
28
+
29
+ asyncio.run(main())
30
+ ```
31
+
32
+ ## Gateway Events
33
+
34
+ ```python
35
+ import asyncio
36
+ from vox_sdk import Client, GatewayClient
37
+
38
+ async def main():
39
+ async with Client("https://vox.example.com") as client:
40
+ await client.login("alice", "password123")
41
+
42
+ info = await client.server.gateway_info()
43
+ gw = GatewayClient(info.url, client.http.token)
44
+
45
+ @gw.on("message_create")
46
+ async def on_message(event):
47
+ print(f"[{event.feed_id}] {event.author_id}: {event.body}")
48
+
49
+ @gw.on("presence_update")
50
+ async def on_presence(event):
51
+ print(f"User {event.user_id} is now {event.status}")
52
+
53
+ # run() auto-reconnects on recoverable errors
54
+ await gw.run()
55
+
56
+ asyncio.run(main())
57
+ ```
58
+
59
+ ## Error Handling
60
+
61
+ ```python
62
+ from vox_sdk import VoxHTTPError, VoxNetworkError
63
+ from vox_sdk.errors import VoxGatewayError
64
+
65
+ try:
66
+ await client.messages.send(feed_id=1, body="hello")
67
+ except VoxHTTPError as e:
68
+ print(f"API error: {e.status} {e.code}")
69
+ except VoxNetworkError as e:
70
+ print(f"Network error: {e}")
71
+ ```
72
+
73
+ ## API Groups
74
+
75
+ The client exposes these API groups as properties:
76
+
77
+ | Property | Description |
78
+ |---|---|
79
+ | `client.auth` | Login, register, MFA, sessions |
80
+ | `client.messages` | Send, edit, delete, reactions, pins |
81
+ | `client.channels` | Feeds, rooms, categories, threads |
82
+ | `client.members` | List, get, ban, kick, update |
83
+ | `client.roles` | Create, assign, permissions |
84
+ | `client.server` | Server info, layout, limits |
85
+ | `client.users` | Profiles, friends, blocks |
86
+ | `client.invites` | Create, resolve, delete |
87
+ | `client.dms` | Open, send, close DMs |
88
+ | `client.voice` | Join, leave, mute, stage |
89
+ | `client.webhooks` | Create, execute webhooks |
90
+ | `client.bots` | Commands, interactions |
91
+ | `client.files` | Upload, download, delete |
92
+ | `client.emoji` | Emoji and stickers |
93
+ | `client.e2ee` | Prekeys, devices, MLS |
94
+ | `client.moderation` | Reports, audit log |
95
+ | `client.federation` | Cross-server federation |
96
+ | `client.search` | Message search |
97
+ | `client.sync` | Offline sync |
98
+ | `client.embeds` | URL embed resolution |
99
+
100
+ ## Models
101
+
102
+ All response models are re-exported from `vox_sdk.models`:
103
+
104
+ ```python
105
+ from vox_sdk.models import MessageResponse, UserResponse, FeedResponse
106
+ ```
@@ -0,0 +1,38 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vox-sdk"
7
+ version = "0.1.0"
8
+ description = "Python client SDK for the Vox protocol"
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "httpx>=0.27",
12
+ "websockets>=13.0",
13
+ "pydantic>=2.0",
14
+ "zstandard>=0.23",
15
+ ]
16
+
17
+ [project.optional-dependencies]
18
+ dev = [
19
+ "pytest>=8.0",
20
+ "pytest-asyncio>=0.24",
21
+ "pytest-cov>=5.0",
22
+ "ruff>=0.6",
23
+ ]
24
+ media = [
25
+ "vox-media>=0.1",
26
+ ]
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["src"]
30
+
31
+ [tool.ruff]
32
+ target-version = "py311"
33
+ line-length = 100
34
+
35
+ [tool.pytest.ini_options]
36
+ asyncio_mode = "auto"
37
+ testpaths = ["tests"]
38
+ addopts = "--cov=vox_sdk --cov-report=term-missing"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
vox_sdk-0.1.0/setup.py ADDED
@@ -0,0 +1,43 @@
1
+ """Custom build hooks — clean stale native extensions before editable installs.
2
+
3
+ maturin develop can leave .so/.pyd files in src/ that shadow pure-Python
4
+ wrappers and cause hard-to-debug import failures. This hooks into both
5
+ ``pip install -e .`` and ``python setup.py develop`` to wipe them first.
6
+ """
7
+
8
+ import glob
9
+ import os
10
+
11
+ from setuptools import setup
12
+ from setuptools.command.build_py import build_py
13
+ from setuptools.command.develop import develop
14
+
15
+ _STALE_PATTERNS = ("src/**/*.so", "src/**/*.dylib", "src/**/*.pyd")
16
+
17
+
18
+ def _clean_stale_extensions() -> None:
19
+ root = os.path.dirname(os.path.abspath(__file__))
20
+ for pattern in _STALE_PATTERNS:
21
+ for path in glob.glob(os.path.join(root, pattern), recursive=True):
22
+ print(f"removing stale native extension: {path}")
23
+ os.remove(path)
24
+
25
+
26
+ class CleanDevelop(develop):
27
+ def run(self) -> None:
28
+ _clean_stale_extensions()
29
+ super().run()
30
+
31
+
32
+ class CleanBuildPy(build_py):
33
+ def run(self) -> None:
34
+ _clean_stale_extensions()
35
+ super().run()
36
+
37
+
38
+ setup(
39
+ cmdclass={
40
+ "develop": CleanDevelop,
41
+ "build_py": CleanBuildPy,
42
+ },
43
+ )
@@ -0,0 +1,15 @@
1
+ """Vox Client SDK — Python client for the Vox protocol."""
2
+
3
+ from vox_sdk.client import Client
4
+ from vox_sdk.gateway import GatewayClient
5
+ from vox_sdk.errors import VoxHTTPError, VoxGatewayError, VoxNetworkError
6
+ from vox_sdk.permissions import Permissions
7
+
8
+ __all__ = [
9
+ "Client",
10
+ "GatewayClient",
11
+ "Permissions",
12
+ "VoxHTTPError",
13
+ "VoxGatewayError",
14
+ "VoxNetworkError",
15
+ ]
@@ -0,0 +1,6 @@
1
+ """Re-export the native vox_media extension as vox_sdk._media."""
2
+
3
+ from vox_media import * # noqa: F401,F403
4
+ from vox_media import VoxMediaClient
5
+
6
+ __all__ = ["VoxMediaClient"]
@@ -0,0 +1 @@
1
+ """SDK API method groups."""
@@ -0,0 +1,153 @@
1
+ """Auth API methods."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from vox_sdk.models.auth import (
8
+ LoginResponse,
9
+ MFARequiredResponse,
10
+ MFASetupConfirmResponse,
11
+ MFASetupResponse,
12
+ MFAStatusResponse,
13
+ RegisterResponse,
14
+ SessionListResponse,
15
+ SuccessResponse,
16
+ WebAuthnChallengeResponse,
17
+ WebAuthnCredentialResponse,
18
+ )
19
+
20
+ if TYPE_CHECKING:
21
+ from vox_sdk.http import HTTPClient
22
+
23
+
24
+ class AuthAPI:
25
+ """Methods for /api/v1/auth endpoints."""
26
+
27
+ def __init__(self, http: HTTPClient) -> None:
28
+ self._http = http
29
+
30
+ async def register(
31
+ self, username: str, password: str, display_name: str | None = None
32
+ ) -> RegisterResponse:
33
+ payload: dict[str, Any] = {"username": username, "password": password}
34
+ if display_name is not None:
35
+ payload["display_name"] = display_name
36
+ r = await self._http.post("/api/v1/auth/register", json=payload)
37
+ return RegisterResponse.model_validate(r.json())
38
+
39
+ async def login(self, username: str, password: str) -> LoginResponse | MFARequiredResponse:
40
+ r = await self._http.post(
41
+ "/api/v1/auth/login", json={"username": username, "password": password}
42
+ )
43
+ data = r.json()
44
+ if data.get("mfa_required"):
45
+ return MFARequiredResponse.model_validate(data)
46
+ return LoginResponse.model_validate(data)
47
+
48
+ async def login_2fa(
49
+ self,
50
+ mfa_ticket: str,
51
+ method: str,
52
+ code: str | None = None,
53
+ assertion: dict | None = None,
54
+ ) -> LoginResponse:
55
+ payload: dict[str, Any] = {"mfa_ticket": mfa_ticket, "method": method}
56
+ if code is not None:
57
+ payload["code"] = code
58
+ if assertion is not None:
59
+ payload["assertion"] = assertion
60
+ r = await self._http.post("/api/v1/auth/login/2fa", json=payload)
61
+ return LoginResponse.model_validate(r.json())
62
+
63
+ async def login_webauthn_challenge(self, username: str) -> WebAuthnChallengeResponse:
64
+ r = await self._http.post(
65
+ "/api/v1/auth/login/webauthn/challenge", json={"username": username}
66
+ )
67
+ return WebAuthnChallengeResponse.model_validate(r.json())
68
+
69
+ async def login_webauthn(
70
+ self,
71
+ username: str,
72
+ challenge_id: str,
73
+ client_data_json: str,
74
+ authenticator_data: str,
75
+ signature: str,
76
+ credential_id: str,
77
+ user_handle: str | None = None,
78
+ ) -> LoginResponse:
79
+ payload: dict[str, Any] = {
80
+ "username": username,
81
+ "challenge_id": challenge_id,
82
+ "client_data_json": client_data_json,
83
+ "authenticator_data": authenticator_data,
84
+ "signature": signature,
85
+ "credential_id": credential_id,
86
+ }
87
+ if user_handle is not None:
88
+ payload["user_handle"] = user_handle
89
+ r = await self._http.post("/api/v1/auth/login/webauthn", json=payload)
90
+ return LoginResponse.model_validate(r.json())
91
+
92
+ async def login_federation(self, federation_token: str) -> LoginResponse:
93
+ r = await self._http.post(
94
+ "/api/v1/auth/login/federation", json={"federation_token": federation_token}
95
+ )
96
+ return LoginResponse.model_validate(r.json())
97
+
98
+ async def logout(self) -> None:
99
+ await self._http.post("/api/v1/auth/logout")
100
+
101
+ async def mfa_status(self) -> MFAStatusResponse:
102
+ r = await self._http.get("/api/v1/auth/2fa")
103
+ return MFAStatusResponse.model_validate(r.json())
104
+
105
+ async def mfa_setup(self, method: str) -> MFASetupResponse:
106
+ r = await self._http.post("/api/v1/auth/2fa/setup", json={"method": method})
107
+ return MFASetupResponse.model_validate(r.json())
108
+
109
+ async def mfa_setup_confirm(
110
+ self,
111
+ setup_id: str,
112
+ code: str | None = None,
113
+ attestation: dict | None = None,
114
+ credential_name: str | None = None,
115
+ ) -> MFASetupConfirmResponse:
116
+ payload: dict[str, Any] = {"setup_id": setup_id}
117
+ if code is not None:
118
+ payload["code"] = code
119
+ if attestation is not None:
120
+ payload["attestation"] = attestation
121
+ if credential_name is not None:
122
+ payload["credential_name"] = credential_name
123
+ r = await self._http.post("/api/v1/auth/2fa/setup/confirm", json=payload)
124
+ return MFASetupConfirmResponse.model_validate(r.json())
125
+
126
+ async def mfa_remove(
127
+ self,
128
+ method: str,
129
+ code: str | None = None,
130
+ assertion: dict | None = None,
131
+ ) -> SuccessResponse:
132
+ payload: dict[str, Any] = {"method": method}
133
+ if code is not None:
134
+ payload["code"] = code
135
+ if assertion is not None:
136
+ payload["assertion"] = assertion
137
+ r = await self._http.delete("/api/v1/auth/2fa", json=payload)
138
+ return SuccessResponse.model_validate(r.json())
139
+
140
+ async def list_webauthn_credentials(self) -> list[WebAuthnCredentialResponse]:
141
+ r = await self._http.get("/api/v1/auth/webauthn/credentials")
142
+ return [WebAuthnCredentialResponse.model_validate(c) for c in r.json()]
143
+
144
+ async def delete_webauthn_credential(self, credential_id: str) -> SuccessResponse:
145
+ r = await self._http.delete(f"/api/v1/auth/webauthn/credentials/{credential_id}")
146
+ return SuccessResponse.model_validate(r.json())
147
+
148
+ async def list_sessions(self) -> SessionListResponse:
149
+ r = await self._http.get("/api/v1/auth/sessions")
150
+ return SessionListResponse.model_validate(r.json())
151
+
152
+ async def revoke_session(self, session_id: int) -> None:
153
+ await self._http.delete(f"/api/v1/auth/sessions/{session_id}")
@@ -0,0 +1,63 @@
1
+ """Bots API methods."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from vox_sdk.models.bots import CommandListResponse, OkResponse
8
+
9
+ if TYPE_CHECKING:
10
+ from vox_sdk.http import HTTPClient
11
+
12
+
13
+ class BotsAPI:
14
+ def __init__(self, http: HTTPClient) -> None:
15
+ self._http = http
16
+
17
+ async def register_commands(
18
+ self, user_id: int, commands: list[dict[str, Any]]
19
+ ) -> OkResponse:
20
+ r = await self._http.put(
21
+ f"/api/v1/bots/{user_id}/commands", json={"commands": commands}
22
+ )
23
+ return OkResponse.model_validate(r.json())
24
+
25
+ async def list_bot_commands(self, user_id: int) -> CommandListResponse:
26
+ r = await self._http.get(f"/api/v1/bots/{user_id}/commands")
27
+ return CommandListResponse.model_validate(r.json())
28
+
29
+ async def deregister_commands(
30
+ self, user_id: int, command_names: list[str]
31
+ ) -> OkResponse:
32
+ r = await self._http.delete(
33
+ f"/api/v1/bots/{user_id}/commands", json={"command_names": command_names}
34
+ )
35
+ return OkResponse.model_validate(r.json())
36
+
37
+ async def list_commands(self) -> CommandListResponse:
38
+ r = await self._http.get("/api/v1/commands")
39
+ return CommandListResponse.model_validate(r.json())
40
+
41
+ async def respond_to_interaction(
42
+ self,
43
+ interaction_id: str,
44
+ *,
45
+ body: str | None = None,
46
+ embeds: list[dict] | None = None,
47
+ components: list[dict] | None = None,
48
+ ephemeral: bool = False,
49
+ ) -> None:
50
+ payload: dict[str, Any] = {"ephemeral": ephemeral}
51
+ if body is not None:
52
+ payload["body"] = body
53
+ if embeds is not None:
54
+ payload["embeds"] = embeds
55
+ if components is not None:
56
+ payload["components"] = components
57
+ await self._http.post(f"/api/v1/interactions/{interaction_id}/response", json=payload)
58
+
59
+ async def component_interaction(self, msg_id: int, component_id: str) -> None:
60
+ await self._http.post(
61
+ "/api/v1/interactions/component",
62
+ json={"msg_id": msg_id, "component_id": component_id},
63
+ )
@@ -0,0 +1,196 @@
1
+ """Channels API methods (feeds, rooms, categories, threads)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from vox_sdk.models.channels import (
8
+ CategoryListResponse,
9
+ CategoryResponse,
10
+ FeedResponse,
11
+ RoomResponse,
12
+ ThreadListResponse,
13
+ ThreadResponse,
14
+ )
15
+ from vox_sdk.models.enums import FeedType, RoomType
16
+
17
+ if TYPE_CHECKING:
18
+ from vox_sdk.http import HTTPClient
19
+
20
+ _UNSET: Any = object()
21
+
22
+
23
+ class ChannelsAPI:
24
+ def __init__(self, http: HTTPClient) -> None:
25
+ self._http = http
26
+
27
+ # --- Feeds ---
28
+
29
+ async def get_feed(self, feed_id: int) -> FeedResponse:
30
+ r = await self._http.get(f"/api/v1/feeds/{feed_id}")
31
+ return FeedResponse.model_validate(r.json())
32
+
33
+ async def create_feed(
34
+ self,
35
+ name: str,
36
+ type: FeedType = FeedType.text,
37
+ *,
38
+ category_id: int | None = None,
39
+ permission_overrides: list[dict] | None = None,
40
+ ) -> FeedResponse:
41
+ payload: dict[str, Any] = {"name": name, "type": type}
42
+ if category_id is not None:
43
+ payload["category_id"] = category_id
44
+ if permission_overrides is not None:
45
+ payload["permission_overrides"] = permission_overrides
46
+ r = await self._http.post("/api/v1/feeds", json=payload)
47
+ return FeedResponse.model_validate(r.json())
48
+
49
+ async def update_feed(
50
+ self,
51
+ feed_id: int,
52
+ *,
53
+ name: str | None = None,
54
+ topic: str | None = None,
55
+ category_id: int | None = _UNSET,
56
+ position: int | None = None,
57
+ ) -> FeedResponse:
58
+ payload: dict[str, Any] = {}
59
+ if name is not None:
60
+ payload["name"] = name
61
+ if topic is not None:
62
+ payload["topic"] = topic
63
+ if category_id is not _UNSET:
64
+ payload["category_id"] = category_id
65
+ if position is not None:
66
+ payload["position"] = position
67
+ r = await self._http.patch(f"/api/v1/feeds/{feed_id}", json=payload)
68
+ return FeedResponse.model_validate(r.json())
69
+
70
+ async def delete_feed(self, feed_id: int) -> None:
71
+ await self._http.delete(f"/api/v1/feeds/{feed_id}")
72
+
73
+ async def subscribe_feed(self, feed_id: int) -> None:
74
+ await self._http.put(f"/api/v1/feeds/{feed_id}/subscribers")
75
+
76
+ async def unsubscribe_feed(self, feed_id: int) -> None:
77
+ await self._http.delete(f"/api/v1/feeds/{feed_id}/subscribers")
78
+
79
+ # --- Rooms ---
80
+
81
+ async def get_room(self, room_id: int) -> RoomResponse:
82
+ r = await self._http.get(f"/api/v1/rooms/{room_id}")
83
+ return RoomResponse.model_validate(r.json())
84
+
85
+ async def create_room(
86
+ self,
87
+ name: str,
88
+ type: RoomType = RoomType.voice,
89
+ *,
90
+ category_id: int | None = None,
91
+ permission_overrides: list[dict] | None = None,
92
+ ) -> RoomResponse:
93
+ payload: dict[str, Any] = {"name": name, "type": type}
94
+ if category_id is not None:
95
+ payload["category_id"] = category_id
96
+ if permission_overrides is not None:
97
+ payload["permission_overrides"] = permission_overrides
98
+ r = await self._http.post("/api/v1/rooms", json=payload)
99
+ return RoomResponse.model_validate(r.json())
100
+
101
+ async def update_room(
102
+ self,
103
+ room_id: int,
104
+ *,
105
+ name: str | None = None,
106
+ category_id: int | None = _UNSET,
107
+ position: int | None = None,
108
+ ) -> RoomResponse:
109
+ payload: dict[str, Any] = {}
110
+ if name is not None:
111
+ payload["name"] = name
112
+ if category_id is not _UNSET:
113
+ payload["category_id"] = category_id
114
+ if position is not None:
115
+ payload["position"] = position
116
+ r = await self._http.patch(f"/api/v1/rooms/{room_id}", json=payload)
117
+ return RoomResponse.model_validate(r.json())
118
+
119
+ async def delete_room(self, room_id: int) -> None:
120
+ await self._http.delete(f"/api/v1/rooms/{room_id}")
121
+
122
+ # --- Categories ---
123
+
124
+ async def list_categories(self) -> CategoryListResponse:
125
+ r = await self._http.get("/api/v1/categories")
126
+ return CategoryListResponse.model_validate(r.json())
127
+
128
+ async def get_category(self, category_id: int) -> CategoryResponse:
129
+ r = await self._http.get(f"/api/v1/categories/{category_id}")
130
+ return CategoryResponse.model_validate(r.json())
131
+
132
+ async def create_category(self, name: str, position: int = 0) -> CategoryResponse:
133
+ r = await self._http.post(
134
+ "/api/v1/categories", json={"name": name, "position": position}
135
+ )
136
+ return CategoryResponse.model_validate(r.json())
137
+
138
+ async def update_category(
139
+ self, category_id: int, *, name: str | None = None, position: int | None = None
140
+ ) -> CategoryResponse:
141
+ payload: dict[str, Any] = {}
142
+ if name is not None:
143
+ payload["name"] = name
144
+ if position is not None:
145
+ payload["position"] = position
146
+ r = await self._http.patch(f"/api/v1/categories/{category_id}", json=payload)
147
+ return CategoryResponse.model_validate(r.json())
148
+
149
+ async def delete_category(self, category_id: int) -> None:
150
+ await self._http.delete(f"/api/v1/categories/{category_id}")
151
+
152
+ # --- Threads ---
153
+
154
+ async def get_thread(self, thread_id: int) -> ThreadResponse:
155
+ r = await self._http.get(f"/api/v1/threads/{thread_id}")
156
+ return ThreadResponse.model_validate(r.json())
157
+
158
+ async def list_threads(self, feed_id: int) -> ThreadListResponse:
159
+ r = await self._http.get(f"/api/v1/feeds/{feed_id}/threads")
160
+ return ThreadListResponse.model_validate(r.json())
161
+
162
+ async def create_thread(
163
+ self, feed_id: int, parent_msg_id: int, name: str
164
+ ) -> ThreadResponse:
165
+ r = await self._http.post(
166
+ f"/api/v1/feeds/{feed_id}/threads",
167
+ json={"parent_msg_id": parent_msg_id, "name": name},
168
+ )
169
+ return ThreadResponse.model_validate(r.json())
170
+
171
+ async def update_thread(
172
+ self,
173
+ thread_id: int,
174
+ *,
175
+ name: str | None = None,
176
+ archived: bool | None = None,
177
+ locked: bool | None = None,
178
+ ) -> ThreadResponse:
179
+ payload: dict[str, Any] = {}
180
+ if name is not None:
181
+ payload["name"] = name
182
+ if archived is not None:
183
+ payload["archived"] = archived
184
+ if locked is not None:
185
+ payload["locked"] = locked
186
+ r = await self._http.patch(f"/api/v1/threads/{thread_id}", json=payload)
187
+ return ThreadResponse.model_validate(r.json())
188
+
189
+ async def delete_thread(self, thread_id: int) -> None:
190
+ await self._http.delete(f"/api/v1/threads/{thread_id}")
191
+
192
+ async def subscribe_thread(self, feed_id: int, thread_id: int) -> None:
193
+ await self._http.put(f"/api/v1/feeds/{feed_id}/threads/{thread_id}/subscribers")
194
+
195
+ async def unsubscribe_thread(self, feed_id: int, thread_id: int) -> None:
196
+ await self._http.delete(f"/api/v1/feeds/{feed_id}/threads/{thread_id}/subscribers")