From b3509611a252b2bbfed86982904fb8bb9a737b8b Mon Sep 17 00:00:00 2001 From: timurgordon Date: Wed, 22 Jan 2025 23:55:18 +0000 Subject: [PATCH] move livekit client to herolib --- lib/clients/livekit/.heroscript | 7 +++ lib/clients/livekit/client.v | 9 ++++ lib/clients/livekit/factory.v | 6 +++ lib/clients/livekit/readme.md | 25 ++++++++++ lib/clients/livekit/room.v | 51 +++++++++++++++++++++ lib/clients/livekit/room_model.v | 33 ++++++++++++++ lib/clients/livekit/room_test.v | 22 +++++++++ lib/clients/livekit/token.v | 36 +++++++++++++++ lib/clients/livekit/token_model.v | 76 +++++++++++++++++++++++++++++++ 9 files changed, 265 insertions(+) create mode 100644 lib/clients/livekit/.heroscript create mode 100644 lib/clients/livekit/client.v create mode 100644 lib/clients/livekit/factory.v create mode 100644 lib/clients/livekit/readme.md create mode 100644 lib/clients/livekit/room.v create mode 100644 lib/clients/livekit/room_model.v create mode 100644 lib/clients/livekit/room_test.v create mode 100644 lib/clients/livekit/token.v create mode 100644 lib/clients/livekit/token_model.v diff --git a/lib/clients/livekit/.heroscript b/lib/clients/livekit/.heroscript new file mode 100644 index 00000000..8fb1278c --- /dev/null +++ b/lib/clients/livekit/.heroscript @@ -0,0 +1,7 @@ + +!!hero_code.generate_client + name:'livekit' + classname:'LivekitClient' + singleton:0 + default:1 + reset:0 \ No newline at end of file diff --git a/lib/clients/livekit/client.v b/lib/clients/livekit/client.v new file mode 100644 index 00000000..44e16c98 --- /dev/null +++ b/lib/clients/livekit/client.v @@ -0,0 +1,9 @@ +module livekit + +// App struct with `livekit.Client`, API keys, and other shared data +pub struct Client { +pub: + url string @[required] + api_key string @[required] + api_secret string @[required] +} diff --git a/lib/clients/livekit/factory.v b/lib/clients/livekit/factory.v new file mode 100644 index 00000000..033eb2f9 --- /dev/null +++ b/lib/clients/livekit/factory.v @@ -0,0 +1,6 @@ + +module livekit + +pub fn new(client Client) Client { + return Client{...client} +} \ No newline at end of file diff --git a/lib/clients/livekit/readme.md b/lib/clients/livekit/readme.md new file mode 100644 index 00000000..e8500dcb --- /dev/null +++ b/lib/clients/livekit/readme.md @@ -0,0 +1,25 @@ +# livekit + +To get started + +```vlang + +import freeflowuniverse.herolib.clients.livekit + +mut client:= livekit.get()! + +client... + +``` + +## example heroscript + + +```hero +!!livekit.configure + livekit_url:'' + livekit_api_key:'' + livekit_api_secret:'' +``` + + diff --git a/lib/clients/livekit/room.v b/lib/clients/livekit/room.v new file mode 100644 index 00000000..cce17e32 --- /dev/null +++ b/lib/clients/livekit/room.v @@ -0,0 +1,51 @@ +module livekit + +import net.http +import json + +@[params] +pub struct ListRoomsParams { + names []string +} + +pub struct ListRoomsResponse { +pub: + rooms []Room +} + +pub fn (c Client) list_rooms(params ListRoomsParams) !ListRoomsResponse { + // Prepare request body + request := params + request_json := json.encode(request) + + + // create token and give grant to list rooms + mut token := c.new_access_token()! + token.grants.video.room_list = true + + // make POST request + url := '${c.url}/twirp/livekit.RoomService/ListRooms' + // Configure HTTP request + mut headers := http.new_header_from_map({ + http.CommonHeader.authorization: 'Bearer ${token.to_jwt()!}', + http.CommonHeader.content_type: 'application/json' + }) + + response := http.fetch(http.FetchConfig{ + url: url + method: .post + header: headers + data: request_json + })! + + if response.status_code != 200 { + return error('Failed to list rooms: $response.status_code') + } + + // Parse response + rooms_response := json.decode(ListRoomsResponse, response.body) or { + return error('Failed to parse response: $err') + } + + return rooms_response +} diff --git a/lib/clients/livekit/room_model.v b/lib/clients/livekit/room_model.v new file mode 100644 index 00000000..119c9a67 --- /dev/null +++ b/lib/clients/livekit/room_model.v @@ -0,0 +1,33 @@ +module livekit + +import net.http +import json + +pub struct Codec { +pub: + fmtp_line string + mime string +} + +pub struct Version { +pub: + ticks u64 + unix_micro string +} + +pub struct Room { +pub: + active_recording bool + creation_time string + departure_timeout int + empty_timeout int + enabled_codecs []Codec + max_participants int + metadata string + name string + num_participants int + num_publishers int + sid string + turn_password string + version Version +} \ No newline at end of file diff --git a/lib/clients/livekit/room_test.v b/lib/clients/livekit/room_test.v new file mode 100644 index 00000000..92c93cab --- /dev/null +++ b/lib/clients/livekit/room_test.v @@ -0,0 +1,22 @@ +module livekit + +import os +import freeflowuniverse.herolib.osal + +fn testsuite_begin() ! { + osal.load_env_file('${os.dir(@FILE)}/.env')! +} + +fn new_test_client() Client { + return new( + url: os.getenv('LIVEKIT_URL') + api_key: os.getenv('LIVEKIT_API_KEY') + api_secret: os.getenv('LIVEKIT_API_SECRET') + ) +} + +fn test_client_list_rooms() ! { + client := new_test_client() + rooms := client.list_rooms()! + panic(rooms) +} diff --git a/lib/clients/livekit/token.v b/lib/clients/livekit/token.v new file mode 100644 index 00000000..d33e73af --- /dev/null +++ b/lib/clients/livekit/token.v @@ -0,0 +1,36 @@ +module livekit + +import time +import rand +import crypto.hmac +import crypto.sha256 +import encoding.base64 +import json + +// Define AccessTokenOptions struct +@[params] +pub struct AccessTokenOptions { + pub mut: + ttl int | string // TTL in seconds or a time span (e.g., '2d', '5h') + name string // Display name for the participant + identity string // Identity of the user + metadata string // Custom metadata to be passed to participants +} + +// Constructor for AccessToken +pub fn (client Client) new_access_token(options AccessTokenOptions) !AccessToken { + ttl := if options.ttl is int { options.ttl } else { 21600 } // Default TTL of 6 hours (21600 seconds) + + return AccessToken{ + api_key: client.api_key + api_secret: client.api_secret + identity: options.identity + ttl: ttl + grants: ClaimGrants{ + exp: time.now().unix()+ttl + iss: client.api_key + sub: options.name + name: options.name + } + } +} \ No newline at end of file diff --git a/lib/clients/livekit/token_model.v b/lib/clients/livekit/token_model.v new file mode 100644 index 00000000..7c64beda --- /dev/null +++ b/lib/clients/livekit/token_model.v @@ -0,0 +1,76 @@ +module livekit + +import time +import rand +import crypto.hmac +import crypto.sha256 +import encoding.base64 +import json + +// Struct representing grants +pub struct ClaimGrants { +pub mut: + video VideoGrant + iss string + exp i64 + nbf int + sub string + name string +} + +// VideoGrant struct placeholder +pub struct VideoGrant { +pub mut: + room string + room_join bool @[json: 'roomJoin'] + room_list bool @[json: 'roomList'] + can_publish bool @[json: 'canPublish'] + can_publish_data bool @[json: 'canPublishData'] + can_subscribe bool @[json: 'canSubscribe'] +} + +// SIPGrant struct placeholder +struct SIPGrant {} + +// AccessToken class +pub struct AccessToken { + mut: + api_key string + api_secret string + grants ClaimGrants + identity string + ttl int | string +} + +// Method to add a video grant to the token +pub fn (mut token AccessToken) add_video_grant(grant VideoGrant) { + token.grants.video = grant +} + +// Method to generate a JWT token +pub fn (token AccessToken) to_jwt() !string { + // Create JWT payload + payload := json.encode(token.grants) + + println('payload: ${payload}') + + // Create JWT header + header := '{"alg":"HS256","typ":"JWT"}' + + // Encode header and payload in base64 + header_encoded := base64.url_encode_str(header) + payload_encoded := base64.url_encode_str(payload) + + // Create the unsigned token + unsigned_token := '${header_encoded}.${payload_encoded}' + + // Create the HMAC-SHA256 signature + signature := hmac.new(token.api_secret.bytes(), unsigned_token.bytes(), sha256.sum, sha256.block_size) + + // Encode the signature in base64 + signature_encoded := base64.url_encode(signature) + + // Create the final JWT + jwt := '${unsigned_token}.${signature_encoded}' + return jwt +} \ No newline at end of file