Files
dumpsterChat/cmd/server/main.go
T
hobokenchicken 8ee0ce657f Phase 2 backend: LiveKit voice integration
Backend:
- internal/voice/client.go: LiveKit token generation, room management, participant listing
- internal/voice/handlers.go: POST /voice/token, GET /voice/rooms/{roomID}/participants
- internal/gateway/events.go: added VOICE_JOIN, VOICE_LEAVE, VOICE_MUTE, VOICE_DEAFEN events
- cmd/server/main.go: wired voice client and routes under /api/v1/voice
- Docker: livekit.yaml config verified

Frontend deps:
- Added livekit-client and @livekit/components-react packages
2026-06-28 16:21:16 -04:00

181 lines
5.0 KiB
Go

package main
import (
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/auth"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/channel"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/db"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/gateway"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/giphy"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/message"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/server"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/upload"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/voice"
"github.com/go-chi/chi/v5"
chimw "github.com/go-chi/chi/v5/middleware"
)
func main() {
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
cfg := config.Load()
database, err := db.New(cfg)
if err != nil {
logger.Error("failed to connect to database", "error", err)
os.Exit(1)
}
defer database.Close()
if err := database.RunMigrations(); err != nil {
logger.Error("failed to run migrations", "error", err)
os.Exit(1)
}
// Session store for middleware
sessionStore := auth.NewSessionStore(database.DB, cfg)
// WebSocket hub
hub := gateway.NewHub(logger)
go hub.Run()
// Giphy client (nil if no API key)
giphyClient := giphy.NewClient(cfg.Giphy.APIKey)
// Upload handler (nil if no MinIO config)
uploadHandler, err := upload.NewHandler(cfg)
if err != nil {
logger.Warn("upload handler not available", "error", err)
}
// Voice client (LiveKit)
voiceClient := voice.NewClient(cfg.LiveKit.APIKey, cfg.LiveKit.Secret, cfg.LiveKit.URL)
if voiceClient == nil {
logger.Warn("voice client not configured (missing LIVEKIT_API_KEY/SECRET)")
}
// Auth handler
authHandler := auth.NewHandler(database.DB, cfg)
r := chi.NewRouter()
r.Use(chimw.Logger)
r.Use(chimw.Recoverer)
r.Use(chimw.RequestID)
// Health check
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
// WebSocket endpoint
r.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
gateway.ServeWS(database.DB, hub, logger, w, r)
})
// API routes
r.Route("/api/v1", func(r chi.Router) {
// Auth (public: register, login, logout)
r.Route("/auth", func(r chi.Router) {
authHandler.RegisterPublicRoutes(r)
})
// Protected routes
r.Group(func(r chi.Router) {
r.Use(middleware.Session(sessionStore, cfg))
r.Use(middleware.RequireAuth)
// Auth (protected: me, update profile)
authHandler.RegisterProtectedRoutes(r)
// Servers
r.Route("/servers", func(r chi.Router) {
server.NewHandler(database.DB).RegisterRoutes(r)
// Channels (nested under servers)
r.Route("/{serverID}/channels", func(r chi.Router) {
channel.NewHandler(database.DB).RegisterRoutes(r)
})
})
// Messages (under channels)
r.Route("/channels/{channelID}/messages", func(r chi.Router) {
message.NewHandler(database.DB, hub).RegisterRoutes(r)
})
// Giphy search
if giphyClient != nil {
r.Get("/gifs/search", func(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query().Get("q")
if query == "" {
http.Error(w, `{"error":"missing query"}`, http.StatusBadRequest)
return
}
gifs, err := giphyClient.Search(query, 20)
if err != nil {
http.Error(w, `{"error":"search failed"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(gifs)
})
r.Get("/gifs/trending", func(w http.ResponseWriter, r *http.Request) {
gifs, err := giphyClient.GetTrending(20)
if err != nil {
http.Error(w, `{"error":"trending failed"}`, http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(gifs)
})
}
// File upload
if uploadHandler != nil {
r.Post("/upload", uploadHandler.Upload)
}
// Voice
if voiceClient != nil {
r.Route("/voice", func(r chi.Router) {
voice.NewHandler(database.DB, voiceClient, hub).RegisterRoutes(r)
})
}
})
})
// File serving (public, for viewing uploaded files)
if uploadHandler != nil {
r.Get("/files/*", uploadHandler.Serve)
}
// Static file serving for production (SPA)
staticDir := "/srv/web"
if _, err := os.Stat(staticDir); err == nil {
fileServer := http.FileServer(http.Dir(staticDir))
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
// If the file exists, serve it; otherwise serve index.html (SPA fallback)
path := staticDir + r.URL.Path
if _, err := os.Stat(path); os.IsNotExist(err) {
http.ServeFile(w, r, staticDir+"/index.html")
return
}
fileServer.ServeHTTP(w, r)
})
}
addr := fmt.Sprintf(":%s", cfg.Port)
logger.Info("starting server", "addr", addr)
if err := http.ListenAndServe(addr, r); err != nil {
logger.Error("server error", "error", err)
os.Exit(1)
}
}