* add token endpoint for livekit * fix: invalid type passed to hook * fix: inject livekit env variables for orion * fix: show controls indicator above stream # shortcut * return livekit server url from api * simple huddle implementation with streams * set human name in livekit room context * simplify deployment tooling * join huddle with audio automatically * feat: show when there is an active huddle This introduces a webhook which listens to events from livekit and updates our firestore stream particle. It keeps the client simple, reacting to changes to firestore docs. * use headphones icon for huddles * fix: screenshare not working in electron The default VideoConference component from livekit doesn't support screenshare in electron. This attempts to compose our own layout with livekit components ourselves and introduces our own flow for screenshare.
165 lines
5.5 KiB
Go
165 lines
5.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
|
|
"cloud.google.com/go/firestore"
|
|
"cloud.google.com/go/storage"
|
|
pbaero "github.com/flowy-live/llink/genproto/aero"
|
|
"github.com/flowy-live/llink/internal"
|
|
"github.com/flowy-live/llink/internal/auth"
|
|
"github.com/flowy-live/llink/internal/db"
|
|
"github.com/flowy-live/llink/internal/depot"
|
|
"github.com/flowy-live/llink/internal/handler"
|
|
"github.com/flowy-live/llink/internal/human"
|
|
"github.com/flowy-live/llink/internal/livekit"
|
|
"github.com/flowy-live/llink/internal/middleware"
|
|
"github.com/flowy-live/llink/internal/network"
|
|
"github.com/flowy-live/llink/internal/particle"
|
|
"github.com/flowy-live/llink/internal/utils"
|
|
"github.com/flowy-live/llink/internal/waitlist"
|
|
"github.com/redis/go-redis/v9"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
const (
|
|
// FIX: Use separate redis instance. We start with higher number because use this same instance in helios.
|
|
REDIS_DATABASE_FOR_AUTH int = 4
|
|
)
|
|
|
|
func redisForAuth() *redis.Client {
|
|
return internal.ConnectAndTestRedis(REDIS_DATABASE_FOR_AUTH)
|
|
}
|
|
|
|
func main() {
|
|
port := utils.MustGetEnv("PORT")
|
|
gcsBucket := utils.MustGetEnv("GCS_BUCKET")
|
|
|
|
// Initialize database
|
|
db.Init()
|
|
defer db.Cleanup()
|
|
|
|
// Initialize Redis for auth
|
|
redisClient := redisForAuth()
|
|
|
|
// Initialize GCS client
|
|
ctx := context.Background()
|
|
storageClient, err := storage.NewClient(ctx)
|
|
if err != nil {
|
|
slog.Error("failed to create GCS client", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
defer storageClient.Close()
|
|
|
|
aeroAddr := utils.MustGetEnv("AERO_ADDR")
|
|
if aeroAddr == "" {
|
|
slog.Error("must provide AERO_ADDR")
|
|
os.Exit(1)
|
|
}
|
|
aeroServer, err := grpc.NewClient(aeroAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
|
|
if err != nil {
|
|
slog.Error("connection to aero server invalid", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
defer aeroServer.Close()
|
|
aeroSvc := pbaero.NewPrimaryClient(aeroServer)
|
|
|
|
// Initialize services
|
|
authSvc := auth.NewAuthService(redisClient, aeroSvc)
|
|
humanSvc := human.NewService(db.Pool())
|
|
networkSvc := network.NewService(db.Pool())
|
|
particleSvc := particle.NewService(db.Pool(), networkSvc)
|
|
depotSvc := depot.NewService(db.Pool(), storageClient, depot.Config{
|
|
GoogleServiceAccountEmail: utils.MustGetEnv("GOOGLE_SERVICE_ACCOUNT_EMAIL"),
|
|
BucketName: gcsBucket,
|
|
})
|
|
waitlistSvc := waitlist.NewService(db.Pool(), aeroSvc)
|
|
livekitClient := livekit.NewClient()
|
|
|
|
// Initialize Firestore client (for webhook-driven updates)
|
|
gcpProject := utils.MustGetEnv("GCP_PROJECT")
|
|
firestoreClient, err := firestore.NewClient(ctx, gcpProject)
|
|
if err != nil {
|
|
slog.Error("failed to create Firestore client", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
defer firestoreClient.Close()
|
|
|
|
// Initialize handler
|
|
h := handler.NewHandler(authSvc, humanSvc, networkSvc, particleSvc, depotSvc, waitlistSvc, livekitClient, firestoreClient)
|
|
|
|
// Helper to wrap handlers with auth middleware
|
|
withAuth := func(hf http.HandlerFunc) http.Handler {
|
|
return middleware.Auth(authSvc)(http.HandlerFunc(hf))
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
// ==========================================================================
|
|
// Public routes (no auth)
|
|
// ==========================================================================
|
|
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
mux.HandleFunc("POST /auth/request-code", h.RequestSignInCode)
|
|
mux.HandleFunc("POST /auth/sign-in", h.SignIn)
|
|
mux.HandleFunc("POST /waitlist", h.AddToWaitlist)
|
|
mux.HandleFunc("POST /livekit/webhook", h.HandleLivekitWebhook)
|
|
|
|
// ==========================================================================
|
|
// Protected routes (auth required)
|
|
// ==========================================================================
|
|
|
|
// Auth
|
|
mux.Handle("POST /auth/sign-out", withAuth(h.SignOut))
|
|
mux.Handle("GET /auth/me", withAuth(h.GetCurrentHuman))
|
|
|
|
// Networks
|
|
mux.Handle("POST /networks", withAuth(h.CreateNetwork))
|
|
mux.Handle("GET /networks", withAuth(h.ListNetworks))
|
|
mux.Handle("GET /networks/{id}", withAuth(h.GetNetwork))
|
|
mux.Handle("POST /networks/{id}/members", withAuth(h.AddMembersToNetwork))
|
|
mux.Handle("PUT /networks/{id}/message-retention", withAuth(h.SetMessageRetentionHours))
|
|
// mux.Handle("DELETE /networks/{id}/members/{humanId}", withAuth(h.RemoveMemberFromNetwork))
|
|
|
|
// Network Invitations
|
|
mux.Handle("GET /networks/{id}/invitations", withAuth(h.ListInvitationsForNetwork))
|
|
mux.Handle("DELETE /networks/{id}/invitations", withAuth(h.RevokeInvitation))
|
|
mux.Handle("GET /invitations", withAuth(h.ListMyInvitations))
|
|
mux.Handle("POST /invitations/accept", withAuth(h.AcceptInvitation))
|
|
|
|
// Particles
|
|
mux.Handle("GET /particles/{id}/download", withAuth(h.DownloadParticleMedia))
|
|
|
|
// Depot
|
|
mux.Handle("POST /depot/upload", withAuth(h.PrepareUpload))
|
|
mux.Handle("POST /depot/objects/{id}/confirm", withAuth(h.ConfirmUpload))
|
|
|
|
// LiveKit
|
|
mux.Handle("POST /livekit/token", withAuth(h.GetLivekitToken))
|
|
|
|
// Waitlist (admin-only)
|
|
mux.Handle("GET /waitlist", withAuth(h.GetWaitlist))
|
|
mux.Handle("GET /waitlist/{email}", withAuth(h.GetWaitlistEntry))
|
|
mux.Handle("POST /waitlist/invite", withAuth(h.InviteWaitlistEntrant))
|
|
|
|
// Apply middleware
|
|
// nil allows all origins (required for electron app)
|
|
muxWithCors := middleware.CORS(nil)(mux)
|
|
|
|
addr := fmt.Sprintf("0.0.0.0:%s", port)
|
|
slog.Info("running server", "addr", addr)
|
|
if err := http.ListenAndServe(addr, muxWithCors); err != nil {
|
|
slog.Error("server failed", "error", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|