Skip to content

Commit ce81435

Browse files
authored
add other api services, token verifier & webhooks (#98)
1 parent 41793b4 commit ce81435

12 files changed

+486
-35
lines changed

dev-requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,6 @@ wheel
66
setuptools
77
twine
88
auditwheel; sys_platform == 'linux'
9-
cibuildwheel
9+
cibuildwheel
10+
11+
pytest

livekit-api/livekit/api/__init__.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717

1818
# flake8: noqa
1919
# re-export packages from protocol
20-
from livekit.protocol import egress
21-
from livekit.protocol import ingress
22-
from livekit.protocol import models
23-
from livekit.protocol import room
20+
from livekit.protocol.egress import *
21+
from livekit.protocol.ingress import *
22+
from livekit.protocol.models import *
23+
from livekit.protocol.room import *
24+
from livekit.protocol.webhook import *
2425

25-
from .access_token import VideoGrants, AccessToken
26-
from .room_service import RoomService
26+
from .livekit_api import LiveKitAPI
27+
from .access_token import VideoGrants, AccessToken, TokenVerifier
28+
from .webhook import WebhookReceiver
2729
from .version import __version__

livekit-api/livekit/api/_service.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from typing import Dict
2+
import aiohttp
23
from abc import ABC
34
from ._twirp_client import TwirpClient
45
from .access_token import AccessToken, VideoGrants
@@ -7,8 +8,10 @@
78

89

910
class Service(ABC):
10-
def __init__(self, host: str, api_key: str, api_secret: str):
11-
self._client = TwirpClient(host, "livekit")
11+
def __init__(
12+
self, host: str, api_key: str, api_secret: str, session: aiohttp.ClientSession
13+
):
14+
self._client = TwirpClient(session, host, "livekit")
1215
self.api_key = api_key
1316
self.api_secret = api_secret
1417

@@ -18,6 +21,3 @@ def _auth_header(self, grants: VideoGrants) -> Dict[str, str]:
1821
headers = {}
1922
headers[AUTHORIZATION] = "Bearer {}".format(token)
2023
return headers
21-
22-
async def aclose(self):
23-
await self._client.aclose()

livekit-api/livekit/api/_twirp_client.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,15 @@ class TwirpErrorCode:
5252

5353

5454
class TwirpClient:
55-
def __init__(self, host: str, pkg: str, prefix: str = DEFAULT_PREFIX) -> None:
55+
def __init__(
56+
self,
57+
session: aiohttp.ClientSession,
58+
host: str,
59+
pkg: str,
60+
prefix: str = DEFAULT_PREFIX,
61+
) -> None:
62+
self._session = aiohttp.ClientSession()
63+
5664
parse_res = urlparse(host)
5765
scheme = parse_res.scheme
5866
if scheme.startswith("ws"):
@@ -62,7 +70,6 @@ def __init__(self, host: str, pkg: str, prefix: str = DEFAULT_PREFIX) -> None:
6270
self.host = host.rstrip("/")
6371
self.pkg = pkg
6472
self.prefix = prefix
65-
self.session = aiohttp.ClientSession()
6673

6774
async def request(
6875
self,
@@ -76,15 +83,12 @@ async def request(
7683
headers["Content-Type"] = "application/protobuf"
7784

7885
serialized_data = data.SerializeToString()
79-
async with self.session.request(
80-
"post", url, headers=headers, data=serialized_data
86+
async with self._session.post(
87+
url, headers=headers, data=serialized_data
8188
) as resp:
8289
if resp.status == 200:
8390
return response_class.FromString(await resp.read())
8491
else:
8592
# when we have an error, Twirp always encode it in json
8693
error_data = await resp.json()
8794
raise TwirpError(error_data["code"], error_data["msg"])
88-
89-
async def aclose(self):
90-
await self.session.close()

livekit-api/livekit/api/access_token.py

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@
1414

1515
import calendar
1616
import dataclasses
17+
import re
1718
import datetime
1819
import os
19-
2020
import jwt
2121

2222
DEFAULT_TTL = datetime.timedelta(hours=6)
23+
DEFAULT_LEEWAY = datetime.timedelta(minutes=1)
2324

2425

2526
@dataclasses.dataclass
@@ -63,6 +64,11 @@ class VideoGrants:
6364

6465
@dataclasses.dataclass
6566
class Claims:
67+
exp: int = 0
68+
iss: str = "" # api key
69+
nbf: int = 0
70+
sub: str = "" # identity
71+
6672
name: str = ""
6773
video: VideoGrants = dataclasses.field(default_factory=VideoGrants)
6874
metadata: str = ""
@@ -110,17 +116,12 @@ def with_sha256(self, sha256: str) -> "AccessToken":
110116
return self
111117

112118
def to_jwt(self) -> str:
113-
def camel_case_dict(data) -> dict:
114-
return {
115-
"".join(
116-
word if i == 0 else word.title()
117-
for i, word in enumerate(key.split("_"))
118-
): value
119-
for key, value in data
120-
if value is not None
121-
}
119+
video = self.claims.video
120+
if video.room_join and (not self.identity or not video.room):
121+
raise ValueError("identity and room must be set when joining a room")
122122

123123
claims = dataclasses.asdict(self.claims)
124+
claims = {camel_to_snake(k): v for k, v in claims.items()}
124125
claims.update(
125126
{
126127
"sub": self.identity,
@@ -129,10 +130,43 @@ def camel_case_dict(data) -> dict:
129130
"exp": calendar.timegm(
130131
(datetime.datetime.utcnow() + self.ttl).utctimetuple()
131132
),
132-
"video": dataclasses.asdict(
133-
self.claims.video, dict_factory=camel_case_dict
134-
),
135133
}
136134
)
137135

138136
return jwt.encode(claims, self.api_secret, algorithm="HS256")
137+
138+
139+
class TokenVerifier:
140+
def __init__(
141+
self,
142+
api_key: str = os.getenv("LIVEKIT_API_KEY", ""),
143+
api_secret: str = os.getenv("LIVEKIT_API_SECRET", ""),
144+
*,
145+
leeway: datetime.timedelta = DEFAULT_LEEWAY,
146+
) -> None:
147+
self.api_key = api_key
148+
self.api_secret = api_secret
149+
self._leeway = leeway
150+
151+
def verify(self, token: str) -> Claims:
152+
claims = jwt.decode(
153+
token,
154+
self.api_secret,
155+
issuer=self.api_key,
156+
algorithms=["HS256"],
157+
leeway=self._leeway.total_seconds(),
158+
)
159+
c = Claims(**claims)
160+
161+
video = claims["video"]
162+
video = {camel_to_snake(k): v for k, v in video.items()}
163+
c.video = VideoGrants(**video)
164+
return c
165+
166+
167+
def camel_to_snake(t: str):
168+
return re.sub(r"(?<!^)(?=[A-Z])", "_", t).lower()
169+
170+
171+
def snake_to_camel(t: str):
172+
return "".join(x.title() for x in t.split("_"))
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import aiohttp
2+
from livekit.protocol import egress as proto_egress
3+
from ._service import Service
4+
from .access_token import VideoGrants
5+
6+
SVC = "Egress"
7+
8+
9+
class EgressService(Service):
10+
def __init__(
11+
self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str
12+
):
13+
super().__init__(session, url, api_key, api_secret)
14+
15+
async def start_room_composite_egress(
16+
self, start: proto_egress.RoomCompositeEgressRequest
17+
) -> proto_egress.EgressInfo:
18+
return await self._client.request(
19+
SVC,
20+
"StartRoomCompositeEgress",
21+
start,
22+
self._auth_header(VideoGrants(room_record=True)),
23+
proto_egress.EgressInfo,
24+
)
25+
26+
async def start_web_egress(
27+
self, start: proto_egress.WebEgressRequest
28+
) -> proto_egress.EgressInfo:
29+
return await self._client.request(
30+
SVC,
31+
"StartWebEgress",
32+
start,
33+
self._auth_header(VideoGrants(room_record=True)),
34+
proto_egress.EgressInfo,
35+
)
36+
37+
async def start_participant_egress(
38+
self, start: proto_egress.ParticipantEgressRequest
39+
) -> proto_egress.EgressInfo:
40+
return await self._client.request(
41+
SVC,
42+
"StartParticipantEgress",
43+
start,
44+
self._auth_header(VideoGrants(room_record=True)),
45+
proto_egress.EgressInfo,
46+
)
47+
48+
async def start_track_composite_egress(
49+
self, start: proto_egress.TrackCompositeEgressRequest
50+
) -> proto_egress.EgressInfo:
51+
return await self._client.request(
52+
SVC,
53+
"StartTrackCompositeEgress",
54+
start,
55+
self._auth_header(VideoGrants(room_record=True)),
56+
proto_egress.EgressInfo,
57+
)
58+
59+
async def start_track_egress(
60+
self, start: proto_egress.TrackEgressRequest
61+
) -> proto_egress.EgressInfo:
62+
return await self._client.request(
63+
SVC,
64+
"StartTrackEgress",
65+
start,
66+
self._auth_header(VideoGrants(room_record=True)),
67+
proto_egress.EgressInfo,
68+
)
69+
70+
async def update_layout(
71+
self, update: proto_egress.UpdateLayoutRequest
72+
) -> proto_egress.EgressInfo:
73+
return await self._client.request(
74+
SVC,
75+
"UpdateLayout",
76+
update,
77+
self._auth_header(VideoGrants(room_record=True)),
78+
proto_egress.EgressInfo,
79+
)
80+
81+
async def update_stream(
82+
self, update: proto_egress.UpdateStreamRequest
83+
) -> proto_egress.EgressInfo:
84+
return await self._client.request(
85+
SVC,
86+
"UpdateStream",
87+
update,
88+
self._auth_header(VideoGrants(room_record=True)),
89+
proto_egress.EgressInfo,
90+
)
91+
92+
async def list_egress(
93+
self, list: proto_egress.ListEgressRequest
94+
) -> proto_egress.ListEgressResponse:
95+
return await self._client.request(
96+
SVC,
97+
"ListEgress",
98+
list,
99+
self._auth_header(VideoGrants(room_record=True)),
100+
proto_egress.ListEgressResponse,
101+
)
102+
103+
async def stop_egress(
104+
self, stop: proto_egress.StopEgressRequest
105+
) -> proto_egress.EgressInfo:
106+
return await self._client.request(
107+
SVC,
108+
"StopEgress",
109+
stop,
110+
self._auth_header(VideoGrants(room_record=True)),
111+
proto_egress.EgressInfo,
112+
)
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import aiohttp
2+
from livekit.protocol import ingress as proto_ingress
3+
from ._service import Service
4+
from .access_token import VideoGrants
5+
6+
SVC = "Ingress"
7+
8+
9+
class IngressService(Service):
10+
def __init__(
11+
self, session: aiohttp.ClientSession, url: str, api_key: str, api_secret: str
12+
):
13+
super().__init__(session, url, api_key, api_secret)
14+
15+
async def create_ingress(
16+
self, create: proto_ingress.CreateIngressRequest
17+
) -> proto_ingress.IngressInfo:
18+
return await self._client.request(
19+
SVC,
20+
"CreateIngress",
21+
create,
22+
self._auth_header(VideoGrants(ingress_admin=True)),
23+
proto_ingress.IngressInfo,
24+
)
25+
26+
async def update_ingress(
27+
self, update: proto_ingress.UpdateIngressRequest
28+
) -> proto_ingress.IngressInfo:
29+
return await self._client.request(
30+
SVC,
31+
"UpdateIngress",
32+
update,
33+
self._auth_header(VideoGrants(ingress_admin=True)),
34+
proto_ingress.IngressInfo,
35+
)
36+
37+
async def list_ingress(
38+
self, list: proto_ingress.ListIngressRequest
39+
) -> proto_ingress.ListIngressResponse:
40+
return await self._client.request(
41+
SVC,
42+
"ListIngress",
43+
list,
44+
self._auth_header(VideoGrants(ingress_admin=True)),
45+
proto_ingress.ListIngressResponse,
46+
)
47+
48+
async def delete_ingress(
49+
self, delete: proto_ingress.DeleteIngressRequest
50+
) -> proto_ingress.IngressInfo:
51+
return await self._client.request(
52+
SVC,
53+
"DeleteIngress",
54+
delete,
55+
self._auth_header(VideoGrants(ingress_admin=True)),
56+
proto_ingress.IngressInfo,
57+
)

0 commit comments

Comments
 (0)