fix(build): make build-web works; gofmt all Go files

This commit is contained in:
2026-06-30 10:01:11 -04:00
parent 30e159cbdb
commit d3b7f39b9e
13 changed files with 479 additions and 480 deletions
+307 -307
View File
@@ -1,307 +1,307 @@
package main
import (
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"strings"
_ "git.dustin.coffee/hobokenchicken/dumpsterChat/docs"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/auth"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/bot"
"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/dm"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/gateway"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/giphy"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/invite"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/message"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/moderation"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/permissions"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/push"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/reaction"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/server"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/upload"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/voice"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/webhook"
"github.com/go-chi/chi/v5"
chimw "github.com/go-chi/chi/v5/middleware"
httpSwagger "github.com/swaggo/http-swagger"
)
// @title Dumpster API
// @version 1.0
// @description A chaotic, self-hosted Discord-like platform API
// @host localhost:8080
// @BasePath /api/v1
// @schemes http https
// @securityDefinitions.apikey SessionAuth
// @in cookie
// @name dumpster_session
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 origin allowlist
wsOrigins := []string{"https://" + cfg.Host}
if cfg.Host == "localhost" {
wsOrigins = append(wsOrigins, "http://localhost:"+cfg.Port)
}
gateway.SetAllowedOrigins(wsOrigins)
// WebSocket hub
hub := gateway.NewHub(database.DB, 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)")
}
// Push notification handler (nil if no VAPID keys)
pushHandler := push.NewHandler(database.DB, cfg.WebPush.PublicKey, cfg.WebPush.PrivateKey, cfg.WebPush.Subject, logger)
if cfg.WebPush.PublicKey == "" {
logger.Warn("push notifications not configured (missing VAPID_PUBLIC_KEY)")
}
// Auth handler
authHandler := auth.NewHandler(database.DB, cfg, hub)
// Permissions checker
permissionsChecker := permissions.NewChecker(database.DB)
memberHandler := server.NewMemberHandler(database.DB)
r := chi.NewRouter()
r.Use(chimw.Logger)
r.Use(chimw.Recoverer)
r.Use(chimw.RequestID)
r.Use(middleware.SecurityHeaders)
// 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, cfg.Session.CookieName)
})
// API routes
r.Route("/api/v1", func(r chi.Router) {
// Auth (public: register, login, logout) with strict rate limiting
r.Route("/auth", func(r chi.Router) {
r.Use(middleware.RateLimit(5, 10)) // 5 req/s, burst 10
authHandler.RegisterPublicRoutes(r)
})
// Protected routes
r.Group(func(r chi.Router) {
r.Use(middleware.Session(sessionStore, cfg))
r.Use(middleware.RequireAuth)
r.Use(middleware.CSRFProtect(cfg.Host, cfg.Port, strings.Split(os.Getenv("DUMPSTER_CSRF_ORIGINS"), ",")))
// Auth (protected: me, update profile)
authHandler.RegisterProtectedRoutes(r)
// Servers
r.Route("/servers", func(r chi.Router) {
serverHandler := server.NewHandler(database.DB)
r.Post("/", serverHandler.Create)
r.Get("/", serverHandler.List)
modHandler := moderation.NewHandler(database.DB, hub)
r.Route("/{serverID}", func(r chi.Router) {
r.Get("/", serverHandler.Get)
r.Patch("/", serverHandler.Update)
r.Delete("/", serverHandler.Delete)
// Server members
memberHandler.RegisterRoutes(r)
// Moderation
r.With(middleware.RequirePermission(permissionsChecker, permissions.KICK_MEMBERS)).Delete("/members/{userID}", modHandler.Kick)
r.With(middleware.RequirePermission(permissionsChecker, permissions.BAN_MEMBERS)).Post("/bans", modHandler.Ban)
r.With(middleware.RequirePermission(permissionsChecker, permissions.BAN_MEMBERS)).Get("/bans", modHandler.ListBans)
r.With(middleware.RequirePermission(permissionsChecker, permissions.BAN_MEMBERS)).Delete("/bans/{userID}", modHandler.Unban)
r.With(middleware.RequirePermission(permissionsChecker, permissions.MUTE_MEMBERS)).Post("/mutes", modHandler.Mute)
r.With(middleware.RequirePermission(permissionsChecker, permissions.MUTE_MEMBERS)).Delete("/mutes/{userID}", modHandler.Unmute)
// Channels (nested under servers)
r.Route("/channels", func(r chi.Router) {
channel.NewHandler(database.DB, permissionsChecker).RegisterRoutes(r)
})
})
})
// Direct messages
dmHandler := dm.NewHandler(database.DB, hub, logger)
r.Route("/conversations", func(r chi.Router) {
dmHandler.RegisterRoutes(r)
})
// Roles and member-role assignment
roleHandler := server.NewRoleHandler(database.DB)
roleHandler.RegisterRoleRoutes(r)
// Messages (under channels)
r.Route("/channels/{channelID}/messages", func(r chi.Router) {
message.NewHandler(database.DB, hub, pushHandler, logger).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)
})
}
// Reactions
r.Route("/messages/{messageID}/reactions", func(r chi.Router) {
reaction.NewHandler(database.DB, hub).RegisterRoutes(r)
})
// Bots
r.Route("/bots", func(r chi.Router) {
bot.NewHandler(database.DB).RegisterRoutes(r)
})
// Bot slash commands
r.Route("/bots/{botID}/commands", func(r chi.Router) {
bot.NewCommandHandler(database.DB).RegisterCommandRoutes(r)
})
// Webhooks (protected: create/list/delete)
webhookHandler := webhook.NewHandler(database.DB, hub)
r.Route("/channels/{channelID}/webhooks", func(r chi.Router) {
webhookHandler.RegisterRoutes(r)
})
r.Delete("/webhooks/{webhookID}", webhookHandler.Delete)
// Invites (rate limited to prevent brute-force join)
inviteHandler := invite.NewHandler(database.DB)
r.Post("/servers/{serverID}/invites", inviteHandler.Create)
r.Get("/invites/{code}", inviteHandler.Get)
r.Route("/invites/{code}/join", func(r chi.Router) {
r.Use(middleware.RateLimit(2, 5))
r.Post("/", inviteHandler.Join)
})
})
})
// File serving (public, for viewing uploaded files)
if uploadHandler != nil {
r.Get("/files/*", uploadHandler.Serve)
}
// Public webhook execution (no auth required)
r.Post("/webhooks/{webhookID}/{token}", func(w http.ResponseWriter, r *http.Request) {
webhook.NewHandler(database.DB, hub).Execute(w, r)
})
// Swagger UI (only accessible from localhost to avoid exposing API docs)
r.Group(func(r chi.Router) {
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host := r.Host
if host == "" {
host = r.Header.Get("Host")
}
if host != "localhost:"+cfg.Port && host != "127.0.0.1:"+cfg.Port && host != "[::1]:"+cfg.Port {
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
return
}
next.ServeHTTP(w, r)
})
})
r.Get("/docs/*", httpSwagger.Handler(
httpSwagger.URL("/docs/swagger.json"),
))
})
// 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)
}
}
package main
import (
"encoding/json"
"fmt"
"log/slog"
"net/http"
"os"
"strings"
_ "git.dustin.coffee/hobokenchicken/dumpsterChat/docs"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/auth"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/bot"
"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/dm"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/gateway"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/giphy"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/invite"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/message"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/moderation"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/permissions"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/push"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/reaction"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/server"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/upload"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/voice"
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/webhook"
"github.com/go-chi/chi/v5"
chimw "github.com/go-chi/chi/v5/middleware"
httpSwagger "github.com/swaggo/http-swagger"
)
// @title Dumpster API
// @version 1.0
// @description A chaotic, self-hosted Discord-like platform API
// @host localhost:8080
// @BasePath /api/v1
// @schemes http https
// @securityDefinitions.apikey SessionAuth
// @in cookie
// @name dumpster_session
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 origin allowlist
wsOrigins := []string{"https://" + cfg.Host}
if cfg.Host == "localhost" {
wsOrigins = append(wsOrigins, "http://localhost:"+cfg.Port)
}
gateway.SetAllowedOrigins(wsOrigins)
// WebSocket hub
hub := gateway.NewHub(database.DB, 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)")
}
// Push notification handler (nil if no VAPID keys)
pushHandler := push.NewHandler(database.DB, cfg.WebPush.PublicKey, cfg.WebPush.PrivateKey, cfg.WebPush.Subject, logger)
if cfg.WebPush.PublicKey == "" {
logger.Warn("push notifications not configured (missing VAPID_PUBLIC_KEY)")
}
// Auth handler
authHandler := auth.NewHandler(database.DB, cfg, hub)
// Permissions checker
permissionsChecker := permissions.NewChecker(database.DB)
memberHandler := server.NewMemberHandler(database.DB)
r := chi.NewRouter()
r.Use(chimw.Logger)
r.Use(chimw.Recoverer)
r.Use(chimw.RequestID)
r.Use(middleware.SecurityHeaders)
// 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, cfg.Session.CookieName)
})
// API routes
r.Route("/api/v1", func(r chi.Router) {
// Auth (public: register, login, logout) with strict rate limiting
r.Route("/auth", func(r chi.Router) {
r.Use(middleware.RateLimit(5, 10)) // 5 req/s, burst 10
authHandler.RegisterPublicRoutes(r)
})
// Protected routes
r.Group(func(r chi.Router) {
r.Use(middleware.Session(sessionStore, cfg))
r.Use(middleware.RequireAuth)
r.Use(middleware.CSRFProtect(cfg.Host, cfg.Port, strings.Split(os.Getenv("DUMPSTER_CSRF_ORIGINS"), ",")))
// Auth (protected: me, update profile)
authHandler.RegisterProtectedRoutes(r)
// Servers
r.Route("/servers", func(r chi.Router) {
serverHandler := server.NewHandler(database.DB)
r.Post("/", serverHandler.Create)
r.Get("/", serverHandler.List)
modHandler := moderation.NewHandler(database.DB, hub)
r.Route("/{serverID}", func(r chi.Router) {
r.Get("/", serverHandler.Get)
r.Patch("/", serverHandler.Update)
r.Delete("/", serverHandler.Delete)
// Server members
memberHandler.RegisterRoutes(r)
// Moderation
r.With(middleware.RequirePermission(permissionsChecker, permissions.KICK_MEMBERS)).Delete("/members/{userID}", modHandler.Kick)
r.With(middleware.RequirePermission(permissionsChecker, permissions.BAN_MEMBERS)).Post("/bans", modHandler.Ban)
r.With(middleware.RequirePermission(permissionsChecker, permissions.BAN_MEMBERS)).Get("/bans", modHandler.ListBans)
r.With(middleware.RequirePermission(permissionsChecker, permissions.BAN_MEMBERS)).Delete("/bans/{userID}", modHandler.Unban)
r.With(middleware.RequirePermission(permissionsChecker, permissions.MUTE_MEMBERS)).Post("/mutes", modHandler.Mute)
r.With(middleware.RequirePermission(permissionsChecker, permissions.MUTE_MEMBERS)).Delete("/mutes/{userID}", modHandler.Unmute)
// Channels (nested under servers)
r.Route("/channels", func(r chi.Router) {
channel.NewHandler(database.DB, permissionsChecker).RegisterRoutes(r)
})
})
})
// Direct messages
dmHandler := dm.NewHandler(database.DB, hub, logger)
r.Route("/conversations", func(r chi.Router) {
dmHandler.RegisterRoutes(r)
})
// Roles and member-role assignment
roleHandler := server.NewRoleHandler(database.DB)
roleHandler.RegisterRoleRoutes(r)
// Messages (under channels)
r.Route("/channels/{channelID}/messages", func(r chi.Router) {
message.NewHandler(database.DB, hub, pushHandler, logger).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)
})
}
// Reactions
r.Route("/messages/{messageID}/reactions", func(r chi.Router) {
reaction.NewHandler(database.DB, hub).RegisterRoutes(r)
})
// Bots
r.Route("/bots", func(r chi.Router) {
bot.NewHandler(database.DB).RegisterRoutes(r)
})
// Bot slash commands
r.Route("/bots/{botID}/commands", func(r chi.Router) {
bot.NewCommandHandler(database.DB).RegisterCommandRoutes(r)
})
// Webhooks (protected: create/list/delete)
webhookHandler := webhook.NewHandler(database.DB, hub)
r.Route("/channels/{channelID}/webhooks", func(r chi.Router) {
webhookHandler.RegisterRoutes(r)
})
r.Delete("/webhooks/{webhookID}", webhookHandler.Delete)
// Invites (rate limited to prevent brute-force join)
inviteHandler := invite.NewHandler(database.DB)
r.Post("/servers/{serverID}/invites", inviteHandler.Create)
r.Get("/invites/{code}", inviteHandler.Get)
r.Route("/invites/{code}/join", func(r chi.Router) {
r.Use(middleware.RateLimit(2, 5))
r.Post("/", inviteHandler.Join)
})
})
})
// File serving (public, for viewing uploaded files)
if uploadHandler != nil {
r.Get("/files/*", uploadHandler.Serve)
}
// Public webhook execution (no auth required)
r.Post("/webhooks/{webhookID}/{token}", func(w http.ResponseWriter, r *http.Request) {
webhook.NewHandler(database.DB, hub).Execute(w, r)
})
// Swagger UI (only accessible from localhost to avoid exposing API docs)
r.Group(func(r chi.Router) {
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
host := r.Host
if host == "" {
host = r.Header.Get("Host")
}
if host != "localhost:"+cfg.Port && host != "127.0.0.1:"+cfg.Port && host != "[::1]:"+cfg.Port {
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
return
}
next.ServeHTTP(w, r)
})
})
r.Get("/docs/*", httpSwagger.Handler(
httpSwagger.URL("/docs/swagger.json"),
))
})
// 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)
}
}
+2 -2
View File
@@ -13,8 +13,8 @@ import (
// APIClient wraps HTTP calls to the dumpster server.
type APIClient struct {
BaseURL string
HTTP *http.Client
BaseURL string
HTTP *http.Client
SessionToken string
}
+12 -12
View File
@@ -3,18 +3,18 @@ package main
import "github.com/charmbracelet/bubbles/key"
type keyMap struct {
Up key.Binding
Down key.Binding
Top key.Binding
Bottom key.Binding
Tab key.Binding
ShiftTab key.Binding
FocusInput key.Binding
Unfocus key.Binding
Quit key.Binding
Help key.Binding
Send key.Binding
MemberToggle key.Binding
Up key.Binding
Down key.Binding
Top key.Binding
Bottom key.Binding
Tab key.Binding
ShiftTab key.Binding
FocusInput key.Binding
Unfocus key.Binding
Quit key.Binding
Help key.Binding
Send key.Binding
MemberToggle key.Binding
ChannelPicker key.Binding
}
+2 -2
View File
@@ -53,8 +53,8 @@ var (
BorderForeground(aqua)
UnfocusedBorder = lipgloss.NewStyle().
Border(lipgloss.NormalBorder(), false, false, true, false).
BorderForeground(bgT)
Border(lipgloss.NormalBorder(), false, false, true, false).
BorderForeground(bgT)
// Server list
ServerActive = lipgloss.NewStyle().
+6 -6
View File
@@ -4,12 +4,12 @@ package main
// VoiceState holds the current voice connection state.
type VoiceState struct {
Connected bool
RoomName string
LiveKitURL string
Token string
Muted bool
Deafened bool
Connected bool
RoomName string
LiveKitURL string
Token string
Muted bool
Deafened bool
Participants []VoiceParticipant
}