livekit-api 0.7.0__tar.gz → 0.8.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.
- {livekit_api-0.7.0 → livekit_api-0.8.0}/PKG-INFO +2 -2
- {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/__init__.py +2 -0
- {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/_service.py +5 -3
- {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/access_token.py +75 -29
- livekit_api-0.8.0/livekit/api/agent_dispatch_service.py +108 -0
- {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/livekit_api.py +8 -0
- {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/sip_service.py +17 -0
- livekit_api-0.8.0/livekit/api/version.py +1 -0
- {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/webhook.py +2 -0
- {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit_api.egg-info/PKG-INFO +2 -2
- {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit_api.egg-info/SOURCES.txt +1 -0
- {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit_api.egg-info/requires.txt +1 -1
- {livekit_api-0.7.0 → livekit_api-0.8.0}/setup.py +1 -1
- {livekit_api-0.7.0 → livekit_api-0.8.0}/tests/test_access_token.py +42 -0
- livekit_api-0.7.0/livekit/api/version.py +0 -1
- {livekit_api-0.7.0 → livekit_api-0.8.0}/README.md +0 -0
- {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/egress_service.py +0 -0
- {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/ingress_service.py +0 -0
- {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/py.typed +0 -0
- {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/room_service.py +0 -0
- {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit/api/twirp_client.py +0 -0
- {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit_api.egg-info/dependency_links.txt +0 -0
- {livekit_api-0.7.0 → livekit_api-0.8.0}/livekit_api.egg-info/top_level.txt +0 -0
- {livekit_api-0.7.0 → livekit_api-0.8.0}/pyproject.toml +0 -0
- {livekit_api-0.7.0 → livekit_api-0.8.0}/setup.cfg +0 -0
- {livekit_api-0.7.0 → livekit_api-0.8.0}/tests/test_webhook.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: livekit-api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Python Server API for LiveKit
|
|
5
5
|
Home-page: https://github.com/livekit/python-sdks
|
|
6
6
|
License: Apache-2.0
|
|
@@ -25,7 +25,7 @@ Requires-Dist: pyjwt>=2.0.0
|
|
|
25
25
|
Requires-Dist: aiohttp>=3.9.0
|
|
26
26
|
Requires-Dist: protobuf>=3
|
|
27
27
|
Requires-Dist: types-protobuf<5,>=4
|
|
28
|
-
Requires-Dist: livekit-protocol<2,>=0.
|
|
28
|
+
Requires-Dist: livekit-protocol<2,>=0.7.0
|
|
29
29
|
|
|
30
30
|
# LiveKit Server APIs
|
|
31
31
|
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
|
|
17
17
|
# flake8: noqa
|
|
18
18
|
# re-export packages from protocol
|
|
19
|
+
from livekit.protocol.agent_dispatch import *
|
|
20
|
+
from livekit.protocol.agent import *
|
|
19
21
|
from livekit.protocol.egress import *
|
|
20
22
|
from livekit.protocol.ingress import *
|
|
21
23
|
from livekit.protocol.models import *
|
|
@@ -18,11 +18,13 @@ class Service(ABC):
|
|
|
18
18
|
self.api_secret = api_secret
|
|
19
19
|
|
|
20
20
|
def _auth_header(
|
|
21
|
-
self, grants: VideoGrants, sip: SIPGrants | None = None
|
|
21
|
+
self, grants: VideoGrants | None, sip: SIPGrants | None = None
|
|
22
22
|
) -> Dict[str, str]:
|
|
23
|
-
tok = AccessToken(self.api_key, self.api_secret)
|
|
23
|
+
tok = AccessToken(self.api_key, self.api_secret)
|
|
24
|
+
if grants:
|
|
25
|
+
tok.with_grants(grants)
|
|
24
26
|
if sip is not None:
|
|
25
|
-
tok
|
|
27
|
+
tok.with_sip_grants(sip)
|
|
26
28
|
|
|
27
29
|
token = tok.to_jwt()
|
|
28
30
|
|
|
@@ -18,7 +18,10 @@ import re
|
|
|
18
18
|
import datetime
|
|
19
19
|
import os
|
|
20
20
|
import jwt
|
|
21
|
-
from typing import Optional, List
|
|
21
|
+
from typing import Optional, List, Literal
|
|
22
|
+
from google.protobuf.json_format import MessageToDict, ParseDict
|
|
23
|
+
|
|
24
|
+
from livekit.protocol.room import RoomConfiguration
|
|
22
25
|
|
|
23
26
|
DEFAULT_TTL = datetime.timedelta(hours=6)
|
|
24
27
|
DEFAULT_LEEWAY = datetime.timedelta(minutes=1)
|
|
@@ -27,13 +30,13 @@ DEFAULT_LEEWAY = datetime.timedelta(minutes=1)
|
|
|
27
30
|
@dataclasses.dataclass
|
|
28
31
|
class VideoGrants:
|
|
29
32
|
# actions on rooms
|
|
30
|
-
room_create: bool =
|
|
31
|
-
room_list: bool =
|
|
32
|
-
room_record: bool =
|
|
33
|
+
room_create: Optional[bool] = None
|
|
34
|
+
room_list: Optional[bool] = None
|
|
35
|
+
room_record: Optional[bool] = None
|
|
33
36
|
|
|
34
37
|
# actions on a particular room
|
|
35
|
-
room_admin: bool =
|
|
36
|
-
room_join: bool =
|
|
38
|
+
room_admin: Optional[bool] = None
|
|
39
|
+
room_join: Optional[bool] = None
|
|
37
40
|
room: str = ""
|
|
38
41
|
|
|
39
42
|
# permissions within a room
|
|
@@ -42,25 +45,24 @@ class VideoGrants:
|
|
|
42
45
|
can_publish_data: bool = True
|
|
43
46
|
|
|
44
47
|
# TrackSource types that a participant may publish.
|
|
45
|
-
# When set, it
|
|
48
|
+
# When set, it supersedes CanPublish. Only sources explicitly set here can be
|
|
46
49
|
# published
|
|
47
|
-
can_publish_sources: List[str] =
|
|
50
|
+
can_publish_sources: Optional[List[str]] = None
|
|
48
51
|
|
|
49
52
|
# by default, a participant is not allowed to update its own metadata
|
|
50
|
-
can_update_own_metadata: bool =
|
|
53
|
+
can_update_own_metadata: Optional[bool] = None
|
|
51
54
|
|
|
52
55
|
# actions on ingresses
|
|
53
|
-
ingress_admin: bool =
|
|
56
|
+
ingress_admin: Optional[bool] = None # applies to all ingress
|
|
54
57
|
|
|
55
58
|
# participant is not visible to other participants (useful when making bots)
|
|
56
|
-
hidden: bool =
|
|
59
|
+
hidden: Optional[bool] = None
|
|
57
60
|
|
|
58
|
-
# indicates to the room that current participant is a recorder
|
|
59
|
-
recorder: bool =
|
|
61
|
+
# [deprecated] indicates to the room that current participant is a recorder
|
|
62
|
+
recorder: Optional[bool] = None
|
|
60
63
|
|
|
61
64
|
# indicates that the holder can register as an Agent framework worker
|
|
62
|
-
|
|
63
|
-
agent: bool = False
|
|
65
|
+
agent: Optional[bool] = None
|
|
64
66
|
|
|
65
67
|
|
|
66
68
|
@dataclasses.dataclass
|
|
@@ -75,14 +77,33 @@ class SIPGrants:
|
|
|
75
77
|
class Claims:
|
|
76
78
|
identity: str = ""
|
|
77
79
|
name: str = ""
|
|
78
|
-
|
|
79
|
-
sip: SIPGrants = dataclasses.field(default_factory=SIPGrants)
|
|
80
|
-
attributes: dict[str, str] = dataclasses.field(default_factory=dict)
|
|
80
|
+
kind: str = ""
|
|
81
81
|
metadata: str = ""
|
|
82
|
-
|
|
82
|
+
video: Optional[VideoGrants] = None
|
|
83
|
+
sip: Optional[SIPGrants] = None
|
|
84
|
+
attributes: Optional[dict[str, str]] = None
|
|
85
|
+
sha256: Optional[str] = None
|
|
86
|
+
room_preset: Optional[str] = None
|
|
87
|
+
room_config: Optional[RoomConfiguration] = None
|
|
88
|
+
|
|
89
|
+
def asdict(self) -> dict:
|
|
90
|
+
# in order to produce minimal JWT size, exclude None or empty values
|
|
91
|
+
claims = dataclasses.asdict(
|
|
92
|
+
self,
|
|
93
|
+
dict_factory=lambda items: {
|
|
94
|
+
snake_to_lower_camel(k): v
|
|
95
|
+
for k, v in items
|
|
96
|
+
if v is not None and v != ""
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
if self.room_config:
|
|
100
|
+
claims["roomConfig"] = MessageToDict(self.room_config)
|
|
101
|
+
return claims
|
|
83
102
|
|
|
84
103
|
|
|
85
104
|
class AccessToken:
|
|
105
|
+
ParticipantKind = Literal["standard", "egress", "ingress", "sip", "agent"]
|
|
106
|
+
|
|
86
107
|
def __init__(
|
|
87
108
|
self,
|
|
88
109
|
api_key: Optional[str] = None,
|
|
@@ -118,6 +139,10 @@ class AccessToken:
|
|
|
118
139
|
self.identity = identity
|
|
119
140
|
return self
|
|
120
141
|
|
|
142
|
+
def with_kind(self, kind: ParticipantKind) -> "AccessToken":
|
|
143
|
+
self.claims.kind = kind
|
|
144
|
+
return self
|
|
145
|
+
|
|
121
146
|
def with_name(self, name: str) -> "AccessToken":
|
|
122
147
|
self.claims.name = name
|
|
123
148
|
return self
|
|
@@ -134,26 +159,36 @@ class AccessToken:
|
|
|
134
159
|
self.claims.sha256 = sha256
|
|
135
160
|
return self
|
|
136
161
|
|
|
162
|
+
def with_room_preset(self, preset: str) -> "AccessToken":
|
|
163
|
+
self.claims.room_preset = preset
|
|
164
|
+
return self
|
|
165
|
+
|
|
166
|
+
def with_room_config(self, config: RoomConfiguration) -> "AccessToken":
|
|
167
|
+
self.claims.room_config = config
|
|
168
|
+
return self
|
|
169
|
+
|
|
137
170
|
def to_jwt(self) -> str:
|
|
138
171
|
video = self.claims.video
|
|
139
|
-
if video.room_join and (not self.identity or not video.room):
|
|
172
|
+
if video and video.room_join and (not self.identity or not video.room):
|
|
140
173
|
raise ValueError("identity and room must be set when joining a room")
|
|
141
174
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
)
|
|
146
|
-
claims.update(
|
|
175
|
+
# we want to exclude None values from the token
|
|
176
|
+
jwt_claims = self.claims.asdict()
|
|
177
|
+
jwt_claims.update(
|
|
147
178
|
{
|
|
148
179
|
"sub": self.identity,
|
|
149
180
|
"iss": self.api_key,
|
|
150
|
-
"nbf": calendar.timegm(
|
|
181
|
+
"nbf": calendar.timegm(
|
|
182
|
+
datetime.datetime.now(datetime.timezone.utc).utctimetuple()
|
|
183
|
+
),
|
|
151
184
|
"exp": calendar.timegm(
|
|
152
|
-
(
|
|
185
|
+
(
|
|
186
|
+
datetime.datetime.now(datetime.timezone.utc) + self.ttl
|
|
187
|
+
).utctimetuple()
|
|
153
188
|
),
|
|
154
189
|
}
|
|
155
190
|
)
|
|
156
|
-
return jwt.encode(
|
|
191
|
+
return jwt.encode(jwt_claims, self.api_secret, algorithm="HS256")
|
|
157
192
|
|
|
158
193
|
|
|
159
194
|
class TokenVerifier:
|
|
@@ -197,7 +232,7 @@ class TokenVerifier:
|
|
|
197
232
|
}
|
|
198
233
|
sip = SIPGrants(**sip_dict)
|
|
199
234
|
|
|
200
|
-
|
|
235
|
+
grant_claims = Claims(
|
|
201
236
|
identity=claims.get("sub", ""),
|
|
202
237
|
name=claims.get("name", ""),
|
|
203
238
|
video=video,
|
|
@@ -207,6 +242,17 @@ class TokenVerifier:
|
|
|
207
242
|
sha256=claims.get("sha256", ""),
|
|
208
243
|
)
|
|
209
244
|
|
|
245
|
+
if claims.get("roomPreset"):
|
|
246
|
+
grant_claims.room_preset = claims.get("roomPreset")
|
|
247
|
+
if claims.get("roomConfig"):
|
|
248
|
+
grant_claims.room_config = ParseDict(
|
|
249
|
+
claims.get("roomConfig"),
|
|
250
|
+
RoomConfiguration(),
|
|
251
|
+
ignore_unknown_fields=True,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return grant_claims
|
|
255
|
+
|
|
210
256
|
|
|
211
257
|
def camel_to_snake(t: str):
|
|
212
258
|
return re.sub(r"(?<!^)(?=[A-Z])", "_", t).lower()
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import aiohttp
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from livekit.protocol import agent_dispatch as proto_agent_dispatch
|
|
4
|
+
from ._service import Service
|
|
5
|
+
from .access_token import VideoGrants
|
|
6
|
+
|
|
7
|
+
SVC = "AgentDispatchService"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AgentDispatchService(Service):
|
|
11
|
+
"""Manage agent dispatches. Service APIs require roomAdmin permissions.
|
|
12
|
+
|
|
13
|
+
An easier way to construct this service is via LiveKitAPI.agent_dispatch.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str
|
|
18
|
+
):
|
|
19
|
+
super().__init__(session, url, api_key, api_secret)
|
|
20
|
+
|
|
21
|
+
async def create_dispatch(
|
|
22
|
+
self, req: proto_agent_dispatch.CreateAgentDispatchRequest
|
|
23
|
+
) -> proto_agent_dispatch.AgentDispatch:
|
|
24
|
+
"""Create an explicit dispatch for an agent to join a room.
|
|
25
|
+
|
|
26
|
+
To use explicit dispatch, your agent must be registered with an `agentName`.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
req (CreateAgentDispatchRequest): Request containing dispatch creation parameters
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
AgentDispatch: The created agent dispatch object
|
|
33
|
+
"""
|
|
34
|
+
return await self._client.request(
|
|
35
|
+
SVC,
|
|
36
|
+
"CreateDispatch",
|
|
37
|
+
req,
|
|
38
|
+
self._auth_header(VideoGrants(room_admin=True, room=req.room)),
|
|
39
|
+
proto_agent_dispatch.AgentDispatch,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
async def delete_dispatch(
|
|
43
|
+
self, dispatch_id: str, room_name: str
|
|
44
|
+
) -> proto_agent_dispatch.AgentDispatch:
|
|
45
|
+
"""Delete an explicit dispatch for an agent in a room.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
dispatch_id (str): ID of the dispatch to delete
|
|
49
|
+
room_name (str): Name of the room containing the dispatch
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
AgentDispatch: The deleted agent dispatch object
|
|
53
|
+
"""
|
|
54
|
+
return await self._client.request(
|
|
55
|
+
SVC,
|
|
56
|
+
"DeleteDispatch",
|
|
57
|
+
proto_agent_dispatch.DeleteAgentDispatchRequest(
|
|
58
|
+
dispatch_id=dispatch_id,
|
|
59
|
+
room=room_name,
|
|
60
|
+
),
|
|
61
|
+
self._auth_header(VideoGrants(room_admin=True, room=room_name)),
|
|
62
|
+
proto_agent_dispatch.AgentDispatch,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
async def list_dispatch(
|
|
66
|
+
self, room_name: str
|
|
67
|
+
) -> list[proto_agent_dispatch.AgentDispatch]:
|
|
68
|
+
"""List all agent dispatches in a room.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
room_name (str): Name of the room to list dispatches from
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
list[AgentDispatch]: List of agent dispatch objects in the room
|
|
75
|
+
"""
|
|
76
|
+
res = await self._client.request(
|
|
77
|
+
SVC,
|
|
78
|
+
"ListDispatch",
|
|
79
|
+
proto_agent_dispatch.ListAgentDispatchRequest(room=room_name),
|
|
80
|
+
self._auth_header(VideoGrants(room_admin=True, room=room_name)),
|
|
81
|
+
proto_agent_dispatch.ListAgentDispatchResponse,
|
|
82
|
+
)
|
|
83
|
+
return list(res.agent_dispatches)
|
|
84
|
+
|
|
85
|
+
async def get_dispatch(
|
|
86
|
+
self, dispatch_id: str, room_name: str
|
|
87
|
+
) -> Optional[proto_agent_dispatch.AgentDispatch]:
|
|
88
|
+
"""Get an Agent dispatch by ID
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
dispatch_id (str): ID of the dispatch to retrieve
|
|
92
|
+
room_name (str): Name of the room containing the dispatch
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Optional[AgentDispatch]: The requested agent dispatch object if found, None otherwise
|
|
96
|
+
"""
|
|
97
|
+
res = await self._client.request(
|
|
98
|
+
SVC,
|
|
99
|
+
"ListDispatch",
|
|
100
|
+
proto_agent_dispatch.ListAgentDispatchRequest(
|
|
101
|
+
dispatch_id=dispatch_id, room=room_name
|
|
102
|
+
),
|
|
103
|
+
self._auth_header(VideoGrants(room_admin=True, room=room_name)),
|
|
104
|
+
proto_agent_dispatch.ListAgentDispatchResponse,
|
|
105
|
+
)
|
|
106
|
+
if len(res.agent_dispatches) > 0:
|
|
107
|
+
return res.agent_dispatches[0]
|
|
108
|
+
return None
|
|
@@ -4,6 +4,7 @@ from .room_service import RoomService
|
|
|
4
4
|
from .egress_service import EgressService
|
|
5
5
|
from .ingress_service import IngressService
|
|
6
6
|
from .sip_service import SipService
|
|
7
|
+
from .agent_dispatch_service import AgentDispatchService
|
|
7
8
|
from typing import Optional
|
|
8
9
|
|
|
9
10
|
|
|
@@ -31,6 +32,13 @@ class LiveKitAPI:
|
|
|
31
32
|
self._ingress = IngressService(self._session, url, api_key, api_secret)
|
|
32
33
|
self._egress = EgressService(self._session, url, api_key, api_secret)
|
|
33
34
|
self._sip = SipService(self._session, url, api_key, api_secret)
|
|
35
|
+
self._agent_dispatch = AgentDispatchService(
|
|
36
|
+
self._session, url, api_key, api_secret
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def agent_dispatch(self):
|
|
41
|
+
return self._agent_dispatch
|
|
34
42
|
|
|
35
43
|
@property
|
|
36
44
|
def room(self):
|
|
@@ -132,3 +132,20 @@ class SipService(Service):
|
|
|
132
132
|
self._auth_header(VideoGrants(), sip=SIPGrants(call=True)),
|
|
133
133
|
proto_sip.SIPParticipantInfo,
|
|
134
134
|
)
|
|
135
|
+
|
|
136
|
+
async def transfer_sip_participant(
|
|
137
|
+
self, transfer: proto_sip.TransferSIPParticipantRequest
|
|
138
|
+
) -> proto_sip.SIPParticipantInfo:
|
|
139
|
+
return await self._client.request(
|
|
140
|
+
SVC,
|
|
141
|
+
"TransferSIPParticipant",
|
|
142
|
+
transfer,
|
|
143
|
+
self._auth_header(
|
|
144
|
+
VideoGrants(
|
|
145
|
+
room_admin=True,
|
|
146
|
+
room=transfer.room_name,
|
|
147
|
+
),
|
|
148
|
+
sip=SIPGrants(call=True),
|
|
149
|
+
),
|
|
150
|
+
proto_sip.SIPParticipantInfo,
|
|
151
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.0"
|
|
@@ -11,6 +11,8 @@ class WebhookReceiver:
|
|
|
11
11
|
|
|
12
12
|
def receive(self, body: str, auth_token: str) -> proto_webhook.WebhookEvent:
|
|
13
13
|
claims = self._verifier.verify(auth_token)
|
|
14
|
+
if claims.sha256 is None:
|
|
15
|
+
raise Exception("sha256 was not found in the token")
|
|
14
16
|
|
|
15
17
|
body_hash = hashlib.sha256(body.encode()).digest()
|
|
16
18
|
claims_hash = base64.b64decode(claims.sha256)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: livekit-api
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Python Server API for LiveKit
|
|
5
5
|
Home-page: https://github.com/livekit/python-sdks
|
|
6
6
|
License: Apache-2.0
|
|
@@ -25,7 +25,7 @@ Requires-Dist: pyjwt>=2.0.0
|
|
|
25
25
|
Requires-Dist: aiohttp>=3.9.0
|
|
26
26
|
Requires-Dist: protobuf>=3
|
|
27
27
|
Requires-Dist: types-protobuf<5,>=4
|
|
28
|
-
Requires-Dist: livekit-protocol<2,>=0.
|
|
28
|
+
Requires-Dist: livekit-protocol<2,>=0.7.0
|
|
29
29
|
|
|
30
30
|
# LiveKit Server APIs
|
|
31
31
|
|
|
@@ -2,6 +2,8 @@ import datetime
|
|
|
2
2
|
|
|
3
3
|
import pytest # type: ignore
|
|
4
4
|
from livekit.api import AccessToken, TokenVerifier, VideoGrants, SIPGrants
|
|
5
|
+
from livekit.protocol.room import RoomConfiguration
|
|
6
|
+
from livekit.protocol.agent_dispatch import RoomAgentDispatch
|
|
5
7
|
|
|
6
8
|
TEST_API_KEY = "myapikey"
|
|
7
9
|
TEST_API_SECRET = "thiskeyistotallyunsafe"
|
|
@@ -32,6 +34,46 @@ def test_verify_token():
|
|
|
32
34
|
assert claims.attributes["key2"] == "value2"
|
|
33
35
|
|
|
34
36
|
|
|
37
|
+
def test_agent_config():
|
|
38
|
+
token = (
|
|
39
|
+
AccessToken(TEST_API_KEY, TEST_API_SECRET)
|
|
40
|
+
.with_identity("test_identity")
|
|
41
|
+
.with_grants(VideoGrants(room_join=True, room="test_room"))
|
|
42
|
+
.with_room_config(
|
|
43
|
+
RoomConfiguration(
|
|
44
|
+
agents=[RoomAgentDispatch(agent_name="test-agent")],
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
.to_jwt()
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
token_verifier = TokenVerifier(TEST_API_KEY, TEST_API_SECRET)
|
|
51
|
+
claims = token_verifier.verify(token)
|
|
52
|
+
# Verify the decoded claims match
|
|
53
|
+
assert claims.room_config.agents[0].agent_name == "test-agent"
|
|
54
|
+
|
|
55
|
+
# Split token into header.payload.signature
|
|
56
|
+
parts = token.split(".")
|
|
57
|
+
import base64
|
|
58
|
+
import json
|
|
59
|
+
|
|
60
|
+
# Decode the payload (middle part)
|
|
61
|
+
payload = parts[1]
|
|
62
|
+
# Add padding if needed
|
|
63
|
+
padding = len(payload) % 4
|
|
64
|
+
if padding:
|
|
65
|
+
payload += "=" * (4 - padding)
|
|
66
|
+
decoded = base64.b64decode(payload)
|
|
67
|
+
payload_json = json.loads(decoded)
|
|
68
|
+
print(decoded)
|
|
69
|
+
|
|
70
|
+
# Verify the room_config and agents were encoded correctly
|
|
71
|
+
assert "roomConfig" in payload_json
|
|
72
|
+
assert "agents" in payload_json["roomConfig"]
|
|
73
|
+
assert len(payload_json["roomConfig"]["agents"]) == 1
|
|
74
|
+
assert payload_json["roomConfig"]["agents"][0]["agentName"] == "test-agent"
|
|
75
|
+
|
|
76
|
+
|
|
35
77
|
def test_verify_token_invalid():
|
|
36
78
|
token = (
|
|
37
79
|
AccessToken(TEST_API_KEY, TEST_API_SECRET)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.7.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|