- Add LiveKit client setup and token generation for room access. - Implement room management functions including CreateRoom, OpenRoom, ListRooms, and DeleteRoom. - Introduce participant management with ListParticipants and RemoveParticipant functions. - Create tests for room and participant management, ensuring proper authentication and authorization. - Add error handling for LiveKit API interactions. - Define data structures for room metadata and token responses. - Implement status endpoint for service availability check.
146 lines
4.3 KiB
Go
146 lines
4.3 KiB
Go
package videochat
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
authsvc "encore.app/auth"
|
|
"encore.dev/beta/auth"
|
|
"encore.dev/beta/errs"
|
|
lkauth "github.com/livekit/protocol/auth"
|
|
"github.com/livekit/protocol/livekit"
|
|
lksdk "github.com/livekit/server-sdk-go/v2"
|
|
"github.com/twitchtv/twirp"
|
|
)
|
|
|
|
var secrets struct {
|
|
LiveKitURL string
|
|
LiveKitAPIKey string
|
|
LiveKitAPISecret string
|
|
}
|
|
|
|
const tokenTTL = 10 * time.Minute
|
|
const apiTimeout = 10 * time.Second
|
|
|
|
func liveKitClient() (*lksdk.RoomServiceClient, string, error) {
|
|
serverURL, err := websocketURL(secrets.LiveKitURL)
|
|
if err != nil || strings.TrimSpace(secrets.LiveKitAPIKey) == "" || strings.TrimSpace(secrets.LiveKitAPISecret) == "" {
|
|
return nil, "", &errs.Error{Code: errs.FailedPrecondition, Message: "LiveKit is not configured"}
|
|
}
|
|
return lksdk.NewRoomServiceClient(serverURL, secrets.LiveKitAPIKey, secrets.LiveKitAPISecret), serverURL, nil
|
|
}
|
|
|
|
func websocketURL(raw string) (string, error) {
|
|
u, err := url.Parse(strings.TrimSpace(raw))
|
|
if err != nil || u.Hostname() == "" || u.User != nil || u.RawQuery != "" || u.Fragment != "" {
|
|
return "", errors.New("invalid LiveKit URL")
|
|
}
|
|
switch u.Scheme {
|
|
case "http", "ws":
|
|
u.Scheme = "ws"
|
|
case "https", "wss":
|
|
u.Scheme = "wss"
|
|
default:
|
|
return "", errors.New("invalid LiveKit URL scheme")
|
|
}
|
|
return strings.TrimRight(u.String(), "/"), nil
|
|
}
|
|
|
|
func requireAdmin() error {
|
|
if _, ok := auth.UserID(); !ok {
|
|
return &errs.Error{Code: errs.Unauthenticated, Message: "must be logged in"}
|
|
}
|
|
data, _ := auth.Data().(*authsvc.AuthData)
|
|
if data == nil || !data.Role.IsAdmin() {
|
|
return &errs.Error{Code: errs.PermissionDenied, Message: "admin role required"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateRoomName(name string) error {
|
|
if strings.TrimSpace(name) == "" || name != strings.TrimSpace(name) || len(name) > 128 || strings.ContainsAny(name, "/\\") || strings.ContainsFunc(name, unicode.IsControl) {
|
|
return &errs.Error{Code: errs.InvalidArgument, Message: "room name must contain 1-128 bytes, without slashes, surrounding whitespace or control characters"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func liveKitError(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
code := errs.Unavailable
|
|
var rpcErr twirp.Error
|
|
if errors.As(err, &rpcErr) {
|
|
switch rpcErr.Code() {
|
|
case twirp.NotFound:
|
|
code = errs.NotFound
|
|
case twirp.InvalidArgument:
|
|
code = errs.InvalidArgument
|
|
case twirp.AlreadyExists:
|
|
code = errs.AlreadyExists
|
|
}
|
|
}
|
|
return errs.WrapCode(err, code, "LiveKit request failed")
|
|
}
|
|
|
|
// Ownership is written only by this server when creating a fresh LiveKit room.
|
|
type broadcastMetadata struct {
|
|
Mode string `json:"mode"`
|
|
OwnerID string `json:"owner_id"`
|
|
DisplayName string `json:"display_name"`
|
|
}
|
|
|
|
func readBroadcast(room *livekit.Room) (broadcastMetadata, bool) {
|
|
var metadata broadcastMetadata
|
|
err := json.Unmarshal([]byte(room.Metadata), &metadata)
|
|
return metadata, err == nil && metadata.Mode == "broadcast" && metadata.OwnerID != "" && metadata.DisplayName != ""
|
|
}
|
|
|
|
func findBroadcast(ctx context.Context, client *lksdk.RoomServiceClient, name string) (*livekit.Room, broadcastMetadata, error) {
|
|
result, err := client.ListRooms(ctx, &livekit.ListRoomsRequest{Names: []string{name}})
|
|
if err != nil {
|
|
return nil, broadcastMetadata{}, liveKitError(err)
|
|
}
|
|
for _, room := range result.Rooms {
|
|
if room.Name == name {
|
|
if metadata, ok := readBroadcast(room); ok {
|
|
return room, metadata, nil
|
|
}
|
|
}
|
|
}
|
|
return nil, broadcastMetadata{}, &errs.Error{Code: errs.NotFound, Message: "broadcast room not found"}
|
|
}
|
|
|
|
func requireRoomOwner(ctx context.Context, client *lksdk.RoomServiceClient, name string) error {
|
|
uid, ok := auth.UserID()
|
|
if !ok {
|
|
return &errs.Error{Code: errs.Unauthenticated, Message: "must be logged in"}
|
|
}
|
|
_, metadata, err := findBroadcast(ctx, client, name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if metadata.OwnerID != string(uid) {
|
|
return &errs.Error{Code: errs.PermissionDenied, Message: "room owner required"}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func signJoinToken(key, secret, identity, room string, owner bool) (string, error) {
|
|
grant := &lkauth.VideoGrant{RoomJoin: true, Room: room}
|
|
grant.SetCanPublish(owner)
|
|
grant.SetCanSubscribe(true)
|
|
grant.SetCanPublishData(false)
|
|
grant.SetCanUpdateOwnMetadata(false)
|
|
return lkauth.NewAccessToken(key, secret).
|
|
SetIdentity(identity).
|
|
SetValidFor(tokenTTL).
|
|
SetVideoGrant(grant).
|
|
ToJWT()
|
|
}
|