1919import os
2020import jwt
2121from typing import Optional , List , Literal
22+ from google .protobuf .json_format import MessageToDict , ParseDict
23+
24+ from livekit .protocol .room import RoomConfiguration
2225
2326DEFAULT_TTL = datetime .timedelta (hours = 6 )
2427DEFAULT_LEEWAY = datetime .timedelta (minutes = 1 )
2730@dataclasses .dataclass
2831class VideoGrants :
2932 # actions on rooms
30- room_create : bool = False
31- room_list : bool = False
32- room_record : bool = False
33+ room_create : Optional [ bool ] = None
34+ room_list : Optional [ bool ] = None
35+ room_record : Optional [ bool ] = None
3336
3437 # actions on a particular room
35- room_admin : bool = False
36- room_join : bool = False
38+ room_admin : Optional [ bool ] = None
39+ room_join : Optional [ bool ] = None
3740 room : str = ""
3841
3942 # permissions within a room
@@ -44,23 +47,22 @@ class VideoGrants:
4447 # TrackSource types that a participant may publish.
4548 # When set, it supersedes CanPublish. Only sources explicitly set here can be
4649 # published
47- can_publish_sources : List [str ] = dataclasses . field ( default_factory = list )
50+ can_publish_sources : Optional [ List [str ]] = None
4851
4952 # by default, a participant is not allowed to update its own metadata
50- can_update_own_metadata : bool = False
53+ can_update_own_metadata : Optional [ bool ] = None
5154
5255 # actions on ingresses
53- ingress_admin : bool = False # applies to all ingress
56+ ingress_admin : Optional [ bool ] = None # applies to all ingress
5457
5558 # participant is not visible to other participants (useful when making bots)
56- hidden : bool = False
59+ hidden : Optional [ bool ] = None
5760
58- # indicates to the room that current participant is a recorder
59- recorder : bool = False
61+ # [deprecated] indicates to the room that current participant is a recorder
62+ recorder : Optional [ bool ] = None
6063
6164 # indicates that the holder can register as an Agent framework worker
62- # it is also set on all participants that are joining as Agent
63- agent : bool = False
65+ agent : Optional [bool ] = None
6466
6567
6668@dataclasses .dataclass
@@ -75,12 +77,28 @@ class SIPGrants:
7577class Claims :
7678 identity : str = ""
7779 name : str = ""
78- video : VideoGrants = dataclasses .field (default_factory = VideoGrants )
79- sip : SIPGrants = dataclasses .field (default_factory = SIPGrants )
80- attributes : dict [str , str ] = dataclasses .field (default_factory = dict )
81- metadata : str = ""
82- sha256 : str = ""
8380 kind : str = ""
81+ metadata : str = ""
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
84102
85103
86104class AccessToken :
@@ -141,16 +159,22 @@ def with_sha256(self, sha256: str) -> "AccessToken":
141159 self .claims .sha256 = sha256
142160 return self
143161
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+
144170 def to_jwt (self ) -> str :
145171 video = self .claims .video
146- 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 ):
147173 raise ValueError ("identity and room must be set when joining a room" )
148174
149- claims = dataclasses .asdict (
150- self .claims ,
151- dict_factory = lambda items : {snake_to_lower_camel (k ): v for k , v in items },
152- )
153- claims .update (
175+ # we want to exclude None values from the token
176+ jwt_claims = self .claims .asdict ()
177+ jwt_claims .update (
154178 {
155179 "sub" : self .identity ,
156180 "iss" : self .api_key ,
@@ -164,7 +188,7 @@ def to_jwt(self) -> str:
164188 ),
165189 }
166190 )
167- return jwt .encode (claims , self .api_secret , algorithm = "HS256" )
191+ return jwt .encode (jwt_claims , self .api_secret , algorithm = "HS256" )
168192
169193
170194class TokenVerifier :
@@ -208,7 +232,7 @@ def verify(self, token: str) -> Claims:
208232 }
209233 sip = SIPGrants (** sip_dict )
210234
211- return Claims (
235+ grant_claims = Claims (
212236 identity = claims .get ("sub" , "" ),
213237 name = claims .get ("name" , "" ),
214238 video = video ,
@@ -218,6 +242,17 @@ def verify(self, token: str) -> Claims:
218242 sha256 = claims .get ("sha256" , "" ),
219243 )
220244
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+
221256
222257def camel_to_snake (t : str ):
223258 return re .sub (r"(?<!^)(?=[A-Z])" , "_" , t ).lower ()
0 commit comments