fix(build): make build-web works; gofmt all Go files
This commit is contained in:
@@ -18,7 +18,7 @@ docs:
|
|||||||
|
|
||||||
# Build the web frontend
|
# Build the web frontend
|
||||||
build-web:
|
build-web:
|
||||||
cd web && [ -s ~/.nvm/nvm.sh ] && . ~/.nvm/nvm.sh && npm run build || (cd web && npm run build)
|
cd web && ([ -s ~/.nvm/nvm.sh ] && . ~/.nvm/nvm.sh && npm run build || npm run build)
|
||||||
|
|
||||||
# Run the server locally (dev mode)
|
# Run the server locally (dev mode)
|
||||||
run: build-server
|
run: build-server
|
||||||
|
|||||||
+307
-307
@@ -1,307 +1,307 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
_ "git.dustin.coffee/hobokenchicken/dumpsterChat/docs"
|
_ "git.dustin.coffee/hobokenchicken/dumpsterChat/docs"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/auth"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/auth"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/bot"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/bot"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/channel"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/channel"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/config"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/db"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/db"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/dm"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/dm"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/gateway"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/gateway"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/giphy"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/giphy"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/invite"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/invite"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/message"
|
"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/middleware"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/moderation"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/permissions"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/permissions"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/push"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/push"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/reaction"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/reaction"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/server"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/server"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/upload"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/upload"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/voice"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/voice"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/webhook"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/webhook"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
chimw "github.com/go-chi/chi/v5/middleware"
|
chimw "github.com/go-chi/chi/v5/middleware"
|
||||||
httpSwagger "github.com/swaggo/http-swagger"
|
httpSwagger "github.com/swaggo/http-swagger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @title Dumpster API
|
// @title Dumpster API
|
||||||
// @version 1.0
|
// @version 1.0
|
||||||
// @description A chaotic, self-hosted Discord-like platform API
|
// @description A chaotic, self-hosted Discord-like platform API
|
||||||
// @host localhost:8080
|
// @host localhost:8080
|
||||||
// @BasePath /api/v1
|
// @BasePath /api/v1
|
||||||
// @schemes http https
|
// @schemes http https
|
||||||
// @securityDefinitions.apikey SessionAuth
|
// @securityDefinitions.apikey SessionAuth
|
||||||
// @in cookie
|
// @in cookie
|
||||||
// @name dumpster_session
|
// @name dumpster_session
|
||||||
func main() {
|
func main() {
|
||||||
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
|
||||||
cfg := config.Load()
|
cfg := config.Load()
|
||||||
|
|
||||||
database, err := db.New(cfg)
|
database, err := db.New(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to connect to database", "error", err)
|
logger.Error("failed to connect to database", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer database.Close()
|
defer database.Close()
|
||||||
|
|
||||||
if err := database.RunMigrations(); err != nil {
|
if err := database.RunMigrations(); err != nil {
|
||||||
logger.Error("failed to run migrations", "error", err)
|
logger.Error("failed to run migrations", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session store for middleware
|
// Session store for middleware
|
||||||
sessionStore := auth.NewSessionStore(database.DB, cfg)
|
sessionStore := auth.NewSessionStore(database.DB, cfg)
|
||||||
|
|
||||||
// WebSocket origin allowlist
|
// WebSocket origin allowlist
|
||||||
wsOrigins := []string{"https://" + cfg.Host}
|
wsOrigins := []string{"https://" + cfg.Host}
|
||||||
if cfg.Host == "localhost" {
|
if cfg.Host == "localhost" {
|
||||||
wsOrigins = append(wsOrigins, "http://localhost:"+cfg.Port)
|
wsOrigins = append(wsOrigins, "http://localhost:"+cfg.Port)
|
||||||
}
|
}
|
||||||
gateway.SetAllowedOrigins(wsOrigins)
|
gateway.SetAllowedOrigins(wsOrigins)
|
||||||
|
|
||||||
// WebSocket hub
|
// WebSocket hub
|
||||||
hub := gateway.NewHub(database.DB, logger)
|
hub := gateway.NewHub(database.DB, logger)
|
||||||
go hub.Run()
|
go hub.Run()
|
||||||
|
|
||||||
// Giphy client (nil if no API key)
|
// Giphy client (nil if no API key)
|
||||||
giphyClient := giphy.NewClient(cfg.Giphy.APIKey)
|
giphyClient := giphy.NewClient(cfg.Giphy.APIKey)
|
||||||
|
|
||||||
// Upload handler (nil if no MinIO config)
|
// Upload handler (nil if no MinIO config)
|
||||||
uploadHandler, err := upload.NewHandler(cfg)
|
uploadHandler, err := upload.NewHandler(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warn("upload handler not available", "error", err)
|
logger.Warn("upload handler not available", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Voice client (LiveKit)
|
// Voice client (LiveKit)
|
||||||
voiceClient := voice.NewClient(cfg.LiveKit.APIKey, cfg.LiveKit.Secret, cfg.LiveKit.URL)
|
voiceClient := voice.NewClient(cfg.LiveKit.APIKey, cfg.LiveKit.Secret, cfg.LiveKit.URL)
|
||||||
if voiceClient == nil {
|
if voiceClient == nil {
|
||||||
logger.Warn("voice client not configured (missing LIVEKIT_API_KEY/SECRET)")
|
logger.Warn("voice client not configured (missing LIVEKIT_API_KEY/SECRET)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push notification handler (nil if no VAPID keys)
|
// Push notification handler (nil if no VAPID keys)
|
||||||
pushHandler := push.NewHandler(database.DB, cfg.WebPush.PublicKey, cfg.WebPush.PrivateKey, cfg.WebPush.Subject, logger)
|
pushHandler := push.NewHandler(database.DB, cfg.WebPush.PublicKey, cfg.WebPush.PrivateKey, cfg.WebPush.Subject, logger)
|
||||||
if cfg.WebPush.PublicKey == "" {
|
if cfg.WebPush.PublicKey == "" {
|
||||||
logger.Warn("push notifications not configured (missing VAPID_PUBLIC_KEY)")
|
logger.Warn("push notifications not configured (missing VAPID_PUBLIC_KEY)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth handler
|
// Auth handler
|
||||||
authHandler := auth.NewHandler(database.DB, cfg, hub)
|
authHandler := auth.NewHandler(database.DB, cfg, hub)
|
||||||
|
|
||||||
// Permissions checker
|
// Permissions checker
|
||||||
permissionsChecker := permissions.NewChecker(database.DB)
|
permissionsChecker := permissions.NewChecker(database.DB)
|
||||||
memberHandler := server.NewMemberHandler(database.DB)
|
memberHandler := server.NewMemberHandler(database.DB)
|
||||||
|
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
r.Use(chimw.Logger)
|
r.Use(chimw.Logger)
|
||||||
r.Use(chimw.Recoverer)
|
r.Use(chimw.Recoverer)
|
||||||
r.Use(chimw.RequestID)
|
r.Use(chimw.RequestID)
|
||||||
r.Use(middleware.SecurityHeaders)
|
r.Use(middleware.SecurityHeaders)
|
||||||
|
|
||||||
// Health check
|
// Health check
|
||||||
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte("ok"))
|
w.Write([]byte("ok"))
|
||||||
})
|
})
|
||||||
|
|
||||||
// WebSocket endpoint
|
// WebSocket endpoint
|
||||||
r.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||||
gateway.ServeWS(database.DB, hub, logger, w, r, cfg.Session.CookieName)
|
gateway.ServeWS(database.DB, hub, logger, w, r, cfg.Session.CookieName)
|
||||||
})
|
})
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
r.Route("/api/v1", func(r chi.Router) {
|
r.Route("/api/v1", func(r chi.Router) {
|
||||||
// Auth (public: register, login, logout) with strict rate limiting
|
// Auth (public: register, login, logout) with strict rate limiting
|
||||||
r.Route("/auth", func(r chi.Router) {
|
r.Route("/auth", func(r chi.Router) {
|
||||||
r.Use(middleware.RateLimit(5, 10)) // 5 req/s, burst 10
|
r.Use(middleware.RateLimit(5, 10)) // 5 req/s, burst 10
|
||||||
authHandler.RegisterPublicRoutes(r)
|
authHandler.RegisterPublicRoutes(r)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Protected routes
|
// Protected routes
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(middleware.Session(sessionStore, cfg))
|
r.Use(middleware.Session(sessionStore, cfg))
|
||||||
r.Use(middleware.RequireAuth)
|
r.Use(middleware.RequireAuth)
|
||||||
r.Use(middleware.CSRFProtect(cfg.Host, cfg.Port, strings.Split(os.Getenv("DUMPSTER_CSRF_ORIGINS"), ",")))
|
r.Use(middleware.CSRFProtect(cfg.Host, cfg.Port, strings.Split(os.Getenv("DUMPSTER_CSRF_ORIGINS"), ",")))
|
||||||
|
|
||||||
// Auth (protected: me, update profile)
|
// Auth (protected: me, update profile)
|
||||||
authHandler.RegisterProtectedRoutes(r)
|
authHandler.RegisterProtectedRoutes(r)
|
||||||
|
|
||||||
// Servers
|
// Servers
|
||||||
r.Route("/servers", func(r chi.Router) {
|
r.Route("/servers", func(r chi.Router) {
|
||||||
serverHandler := server.NewHandler(database.DB)
|
serverHandler := server.NewHandler(database.DB)
|
||||||
r.Post("/", serverHandler.Create)
|
r.Post("/", serverHandler.Create)
|
||||||
r.Get("/", serverHandler.List)
|
r.Get("/", serverHandler.List)
|
||||||
|
|
||||||
modHandler := moderation.NewHandler(database.DB, hub)
|
modHandler := moderation.NewHandler(database.DB, hub)
|
||||||
|
|
||||||
r.Route("/{serverID}", func(r chi.Router) {
|
r.Route("/{serverID}", func(r chi.Router) {
|
||||||
r.Get("/", serverHandler.Get)
|
r.Get("/", serverHandler.Get)
|
||||||
r.Patch("/", serverHandler.Update)
|
r.Patch("/", serverHandler.Update)
|
||||||
r.Delete("/", serverHandler.Delete)
|
r.Delete("/", serverHandler.Delete)
|
||||||
|
|
||||||
// Server members
|
// Server members
|
||||||
memberHandler.RegisterRoutes(r)
|
memberHandler.RegisterRoutes(r)
|
||||||
|
|
||||||
// Moderation
|
// Moderation
|
||||||
r.With(middleware.RequirePermission(permissionsChecker, permissions.KICK_MEMBERS)).Delete("/members/{userID}", modHandler.Kick)
|
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)).Post("/bans", modHandler.Ban)
|
||||||
r.With(middleware.RequirePermission(permissionsChecker, permissions.BAN_MEMBERS)).Get("/bans", modHandler.ListBans)
|
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.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)).Post("/mutes", modHandler.Mute)
|
||||||
r.With(middleware.RequirePermission(permissionsChecker, permissions.MUTE_MEMBERS)).Delete("/mutes/{userID}", modHandler.Unmute)
|
r.With(middleware.RequirePermission(permissionsChecker, permissions.MUTE_MEMBERS)).Delete("/mutes/{userID}", modHandler.Unmute)
|
||||||
|
|
||||||
// Channels (nested under servers)
|
// Channels (nested under servers)
|
||||||
r.Route("/channels", func(r chi.Router) {
|
r.Route("/channels", func(r chi.Router) {
|
||||||
channel.NewHandler(database.DB, permissionsChecker).RegisterRoutes(r)
|
channel.NewHandler(database.DB, permissionsChecker).RegisterRoutes(r)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Direct messages
|
// Direct messages
|
||||||
dmHandler := dm.NewHandler(database.DB, hub, logger)
|
dmHandler := dm.NewHandler(database.DB, hub, logger)
|
||||||
r.Route("/conversations", func(r chi.Router) {
|
r.Route("/conversations", func(r chi.Router) {
|
||||||
dmHandler.RegisterRoutes(r)
|
dmHandler.RegisterRoutes(r)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Roles and member-role assignment
|
// Roles and member-role assignment
|
||||||
roleHandler := server.NewRoleHandler(database.DB)
|
roleHandler := server.NewRoleHandler(database.DB)
|
||||||
roleHandler.RegisterRoleRoutes(r)
|
roleHandler.RegisterRoleRoutes(r)
|
||||||
|
|
||||||
// Messages (under channels)
|
// Messages (under channels)
|
||||||
r.Route("/channels/{channelID}/messages", func(r chi.Router) {
|
r.Route("/channels/{channelID}/messages", func(r chi.Router) {
|
||||||
message.NewHandler(database.DB, hub, pushHandler, logger).RegisterRoutes(r)
|
message.NewHandler(database.DB, hub, pushHandler, logger).RegisterRoutes(r)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Giphy search
|
// Giphy search
|
||||||
if giphyClient != nil {
|
if giphyClient != nil {
|
||||||
r.Get("/gifs/search", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/gifs/search", func(w http.ResponseWriter, r *http.Request) {
|
||||||
query := r.URL.Query().Get("q")
|
query := r.URL.Query().Get("q")
|
||||||
if query == "" {
|
if query == "" {
|
||||||
http.Error(w, `{"error":"missing query"}`, http.StatusBadRequest)
|
http.Error(w, `{"error":"missing query"}`, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
gifs, err := giphyClient.Search(query, 20)
|
gifs, err := giphyClient.Search(query, 20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, `{"error":"search failed"}`, http.StatusInternalServerError)
|
http.Error(w, `{"error":"search failed"}`, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(gifs)
|
json.NewEncoder(w).Encode(gifs)
|
||||||
})
|
})
|
||||||
r.Get("/gifs/trending", func(w http.ResponseWriter, r *http.Request) {
|
r.Get("/gifs/trending", func(w http.ResponseWriter, r *http.Request) {
|
||||||
gifs, err := giphyClient.GetTrending(20)
|
gifs, err := giphyClient.GetTrending(20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, `{"error":"trending failed"}`, http.StatusInternalServerError)
|
http.Error(w, `{"error":"trending failed"}`, http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
json.NewEncoder(w).Encode(gifs)
|
json.NewEncoder(w).Encode(gifs)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// File upload
|
// File upload
|
||||||
if uploadHandler != nil {
|
if uploadHandler != nil {
|
||||||
r.Post("/upload", uploadHandler.Upload)
|
r.Post("/upload", uploadHandler.Upload)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Voice
|
// Voice
|
||||||
if voiceClient != nil {
|
if voiceClient != nil {
|
||||||
r.Route("/voice", func(r chi.Router) {
|
r.Route("/voice", func(r chi.Router) {
|
||||||
voice.NewHandler(database.DB, voiceClient, hub).RegisterRoutes(r)
|
voice.NewHandler(database.DB, voiceClient, hub).RegisterRoutes(r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reactions
|
// Reactions
|
||||||
r.Route("/messages/{messageID}/reactions", func(r chi.Router) {
|
r.Route("/messages/{messageID}/reactions", func(r chi.Router) {
|
||||||
reaction.NewHandler(database.DB, hub).RegisterRoutes(r)
|
reaction.NewHandler(database.DB, hub).RegisterRoutes(r)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Bots
|
// Bots
|
||||||
r.Route("/bots", func(r chi.Router) {
|
r.Route("/bots", func(r chi.Router) {
|
||||||
bot.NewHandler(database.DB).RegisterRoutes(r)
|
bot.NewHandler(database.DB).RegisterRoutes(r)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Bot slash commands
|
// Bot slash commands
|
||||||
r.Route("/bots/{botID}/commands", func(r chi.Router) {
|
r.Route("/bots/{botID}/commands", func(r chi.Router) {
|
||||||
bot.NewCommandHandler(database.DB).RegisterCommandRoutes(r)
|
bot.NewCommandHandler(database.DB).RegisterCommandRoutes(r)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Webhooks (protected: create/list/delete)
|
// Webhooks (protected: create/list/delete)
|
||||||
webhookHandler := webhook.NewHandler(database.DB, hub)
|
webhookHandler := webhook.NewHandler(database.DB, hub)
|
||||||
r.Route("/channels/{channelID}/webhooks", func(r chi.Router) {
|
r.Route("/channels/{channelID}/webhooks", func(r chi.Router) {
|
||||||
webhookHandler.RegisterRoutes(r)
|
webhookHandler.RegisterRoutes(r)
|
||||||
})
|
})
|
||||||
r.Delete("/webhooks/{webhookID}", webhookHandler.Delete)
|
r.Delete("/webhooks/{webhookID}", webhookHandler.Delete)
|
||||||
|
|
||||||
// Invites (rate limited to prevent brute-force join)
|
// Invites (rate limited to prevent brute-force join)
|
||||||
inviteHandler := invite.NewHandler(database.DB)
|
inviteHandler := invite.NewHandler(database.DB)
|
||||||
r.Post("/servers/{serverID}/invites", inviteHandler.Create)
|
r.Post("/servers/{serverID}/invites", inviteHandler.Create)
|
||||||
r.Get("/invites/{code}", inviteHandler.Get)
|
r.Get("/invites/{code}", inviteHandler.Get)
|
||||||
r.Route("/invites/{code}/join", func(r chi.Router) {
|
r.Route("/invites/{code}/join", func(r chi.Router) {
|
||||||
r.Use(middleware.RateLimit(2, 5))
|
r.Use(middleware.RateLimit(2, 5))
|
||||||
r.Post("/", inviteHandler.Join)
|
r.Post("/", inviteHandler.Join)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// File serving (public, for viewing uploaded files)
|
// File serving (public, for viewing uploaded files)
|
||||||
if uploadHandler != nil {
|
if uploadHandler != nil {
|
||||||
r.Get("/files/*", uploadHandler.Serve)
|
r.Get("/files/*", uploadHandler.Serve)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public webhook execution (no auth required)
|
// Public webhook execution (no auth required)
|
||||||
r.Post("/webhooks/{webhookID}/{token}", func(w http.ResponseWriter, r *http.Request) {
|
r.Post("/webhooks/{webhookID}/{token}", func(w http.ResponseWriter, r *http.Request) {
|
||||||
webhook.NewHandler(database.DB, hub).Execute(w, r)
|
webhook.NewHandler(database.DB, hub).Execute(w, r)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Swagger UI (only accessible from localhost to avoid exposing API docs)
|
// Swagger UI (only accessible from localhost to avoid exposing API docs)
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Use(func(next http.Handler) http.Handler {
|
r.Use(func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
host := r.Host
|
host := r.Host
|
||||||
if host == "" {
|
if host == "" {
|
||||||
host = r.Header.Get("Host")
|
host = r.Header.Get("Host")
|
||||||
}
|
}
|
||||||
if host != "localhost:"+cfg.Port && host != "127.0.0.1:"+cfg.Port && host != "[::1]:"+cfg.Port {
|
if host != "localhost:"+cfg.Port && host != "127.0.0.1:"+cfg.Port && host != "[::1]:"+cfg.Port {
|
||||||
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
|
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
r.Get("/docs/*", httpSwagger.Handler(
|
r.Get("/docs/*", httpSwagger.Handler(
|
||||||
httpSwagger.URL("/docs/swagger.json"),
|
httpSwagger.URL("/docs/swagger.json"),
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Static file serving for production (SPA)
|
// Static file serving for production (SPA)
|
||||||
staticDir := "/srv/web"
|
staticDir := "/srv/web"
|
||||||
if _, err := os.Stat(staticDir); err == nil {
|
if _, err := os.Stat(staticDir); err == nil {
|
||||||
fileServer := http.FileServer(http.Dir(staticDir))
|
fileServer := http.FileServer(http.Dir(staticDir))
|
||||||
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// If the file exists, serve it; otherwise serve index.html (SPA fallback)
|
// If the file exists, serve it; otherwise serve index.html (SPA fallback)
|
||||||
path := staticDir + r.URL.Path
|
path := staticDir + r.URL.Path
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
http.ServeFile(w, r, staticDir+"/index.html")
|
http.ServeFile(w, r, staticDir+"/index.html")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fileServer.ServeHTTP(w, r)
|
fileServer.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
addr := fmt.Sprintf(":%s", cfg.Port)
|
addr := fmt.Sprintf(":%s", cfg.Port)
|
||||||
logger.Info("starting server", "addr", addr)
|
logger.Info("starting server", "addr", addr)
|
||||||
if err := http.ListenAndServe(addr, r); err != nil {
|
if err := http.ListenAndServe(addr, r); err != nil {
|
||||||
logger.Error("server error", "error", err)
|
logger.Error("server error", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -13,8 +13,8 @@ import (
|
|||||||
|
|
||||||
// APIClient wraps HTTP calls to the dumpster server.
|
// APIClient wraps HTTP calls to the dumpster server.
|
||||||
type APIClient struct {
|
type APIClient struct {
|
||||||
BaseURL string
|
BaseURL string
|
||||||
HTTP *http.Client
|
HTTP *http.Client
|
||||||
SessionToken string
|
SessionToken string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -3,18 +3,18 @@ package main
|
|||||||
import "github.com/charmbracelet/bubbles/key"
|
import "github.com/charmbracelet/bubbles/key"
|
||||||
|
|
||||||
type keyMap struct {
|
type keyMap struct {
|
||||||
Up key.Binding
|
Up key.Binding
|
||||||
Down key.Binding
|
Down key.Binding
|
||||||
Top key.Binding
|
Top key.Binding
|
||||||
Bottom key.Binding
|
Bottom key.Binding
|
||||||
Tab key.Binding
|
Tab key.Binding
|
||||||
ShiftTab key.Binding
|
ShiftTab key.Binding
|
||||||
FocusInput key.Binding
|
FocusInput key.Binding
|
||||||
Unfocus key.Binding
|
Unfocus key.Binding
|
||||||
Quit key.Binding
|
Quit key.Binding
|
||||||
Help key.Binding
|
Help key.Binding
|
||||||
Send key.Binding
|
Send key.Binding
|
||||||
MemberToggle key.Binding
|
MemberToggle key.Binding
|
||||||
ChannelPicker key.Binding
|
ChannelPicker key.Binding
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -53,8 +53,8 @@ var (
|
|||||||
BorderForeground(aqua)
|
BorderForeground(aqua)
|
||||||
|
|
||||||
UnfocusedBorder = lipgloss.NewStyle().
|
UnfocusedBorder = lipgloss.NewStyle().
|
||||||
Border(lipgloss.NormalBorder(), false, false, true, false).
|
Border(lipgloss.NormalBorder(), false, false, true, false).
|
||||||
BorderForeground(bgT)
|
BorderForeground(bgT)
|
||||||
|
|
||||||
// Server list
|
// Server list
|
||||||
ServerActive = lipgloss.NewStyle().
|
ServerActive = lipgloss.NewStyle().
|
||||||
|
|||||||
+6
-6
@@ -4,12 +4,12 @@ package main
|
|||||||
|
|
||||||
// VoiceState holds the current voice connection state.
|
// VoiceState holds the current voice connection state.
|
||||||
type VoiceState struct {
|
type VoiceState struct {
|
||||||
Connected bool
|
Connected bool
|
||||||
RoomName string
|
RoomName string
|
||||||
LiveKitURL string
|
LiveKitURL string
|
||||||
Token string
|
Token string
|
||||||
Muted bool
|
Muted bool
|
||||||
Deafened bool
|
Deafened bool
|
||||||
Participants []VoiceParticipant
|
Participants []VoiceParticipant
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+10
-10
@@ -17,8 +17,8 @@ import (
|
|||||||
|
|
||||||
// Handler handles direct-message conversations.
|
// Handler handles direct-message conversations.
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
hub *gateway.Hub
|
hub *gateway.Hub
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,14 +256,14 @@ func (h *Handler) buildResponse(ctx context.Context, convID string) (conversatio
|
|||||||
}
|
}
|
||||||
|
|
||||||
type messageResponse struct {
|
type messageResponse struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
ConversationID string `json:"conversation_id"`
|
ConversationID string `json:"conversation_id"`
|
||||||
AuthorID string `json:"author_id"`
|
AuthorID string `json:"author_id"`
|
||||||
AuthorName string `json:"author_username"`
|
AuthorName string `json:"author_username"`
|
||||||
DisplayName *string `json:"author_display_name"`
|
DisplayName *string `json:"author_display_name"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
EditedAt *string `json:"edited_at"`
|
EditedAt *string `json:"edited_at"`
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListMessages lists messages in a conversation.
|
// ListMessages lists messages in a conversation.
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ import (
|
|||||||
|
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/embed"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/embed"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/gateway"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/gateway"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/moderation"
|
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/middleware"
|
||||||
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/moderation"
|
||||||
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/push"
|
"git.dustin.coffee/hobokenchicken/dumpsterChat/internal/push"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
@@ -136,8 +136,8 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusTooManyRequests)
|
w.WriteHeader(http.StatusTooManyRequests)
|
||||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
"error": "slowmode",
|
"error": "slowmode",
|
||||||
"retry_after": retryAfter,
|
"retry_after": retryAfter,
|
||||||
"retry_after_ms": retryAfter * 1000,
|
"retry_after_ms": retryAfter * 1000,
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|||||||
+105
-105
@@ -1,105 +1,105 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CSRFProtect returns middleware that validates the Origin header on
|
// CSRFProtect returns middleware that validates the Origin header on
|
||||||
// state-changing methods (POST, PUT, PATCH, DELETE).
|
// state-changing methods (POST, PUT, PATCH, DELETE).
|
||||||
//
|
//
|
||||||
// An origin is trusted if:
|
// An origin is trusted if:
|
||||||
// 1. It's in the explicit additionalOrigins list, OR
|
// 1. It's in the explicit additionalOrigins list, OR
|
||||||
// 2. Its hostname matches the configured host, OR
|
// 2. Its hostname matches the configured host, OR
|
||||||
// 3. Its hostname matches the request's own Host header (same-origin check)
|
// 3. Its hostname matches the request's own Host header (same-origin check)
|
||||||
func CSRFProtect(host, port string, additionalOrigins []string) func(http.Handler) http.Handler {
|
func CSRFProtect(host, port string, additionalOrigins []string) func(http.Handler) http.Handler {
|
||||||
originSet := make(map[string]struct{})
|
originSet := make(map[string]struct{})
|
||||||
for _, o := range additionalOrigins {
|
for _, o := range additionalOrigins {
|
||||||
o = strings.TrimSpace(o)
|
o = strings.TrimSpace(o)
|
||||||
if o != "" {
|
if o != "" {
|
||||||
originSet[o] = struct{}{}
|
originSet[o] = struct{}{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hostname := host
|
hostname := host
|
||||||
if idx := strings.LastIndex(hostname, ":"); idx > 0 {
|
if idx := strings.LastIndex(hostname, ":"); idx > 0 {
|
||||||
hostname = hostname[:idx]
|
hostname = hostname[:idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
case "POST", "PUT", "PATCH", "DELETE":
|
case "POST", "PUT", "PATCH", "DELETE":
|
||||||
origin := r.Header.Get("Origin")
|
origin := r.Header.Get("Origin")
|
||||||
if origin == "" {
|
if origin == "" {
|
||||||
referer := r.Header.Get("Referer")
|
referer := r.Header.Get("Referer")
|
||||||
if referer != "" {
|
if referer != "" {
|
||||||
for o := range originSet {
|
for o := range originSet {
|
||||||
if strings.HasPrefix(referer, o) {
|
if strings.HasPrefix(referer, o) {
|
||||||
origin = o
|
origin = o
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if origin != "" {
|
if origin != "" {
|
||||||
// Extract request host for same-origin check
|
// Extract request host for same-origin check
|
||||||
reqHost := r.Host
|
reqHost := r.Host
|
||||||
if idx := strings.LastIndex(reqHost, ":"); idx > 0 {
|
if idx := strings.LastIndex(reqHost, ":"); idx > 0 {
|
||||||
reqHost = reqHost[:idx]
|
reqHost = reqHost[:idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isOriginTrusted(origin, hostname, port, reqHost, originSet) {
|
if !isOriginTrusted(origin, hostname, port, reqHost, originSet) {
|
||||||
http.Error(w, `{"error":"forbidden origin"}`, http.StatusForbidden)
|
http.Error(w, `{"error":"forbidden origin"}`, http.StatusForbidden)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func isOriginTrusted(origin, host, port, reqHost string, originSet map[string]struct{}) bool {
|
func isOriginTrusted(origin, host, port, reqHost string, originSet map[string]struct{}) bool {
|
||||||
if _, ok := originSet[origin]; ok {
|
if _, ok := originSet[origin]; ok {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
withoutScheme := origin
|
withoutScheme := origin
|
||||||
if idx := strings.Index(withoutScheme, "://"); idx >= 0 {
|
if idx := strings.Index(withoutScheme, "://"); idx >= 0 {
|
||||||
withoutScheme = withoutScheme[idx+3:]
|
withoutScheme = withoutScheme[idx+3:]
|
||||||
}
|
}
|
||||||
if idx := strings.Index(withoutScheme, "/"); idx >= 0 {
|
if idx := strings.Index(withoutScheme, "/"); idx >= 0 {
|
||||||
withoutScheme = withoutScheme[:idx]
|
withoutScheme = withoutScheme[:idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
originHost := withoutScheme
|
originHost := withoutScheme
|
||||||
originPort := ""
|
originPort := ""
|
||||||
if idx := strings.LastIndex(originHost, ":"); idx >= 0 {
|
if idx := strings.LastIndex(originHost, ":"); idx >= 0 {
|
||||||
originPort = originHost[idx+1:]
|
originPort = originHost[idx+1:]
|
||||||
originHost = originHost[:idx]
|
originHost = originHost[:idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
originHost = strings.TrimPrefix(originHost, "[")
|
originHost = strings.TrimPrefix(originHost, "[")
|
||||||
originHost = strings.TrimSuffix(originHost, "]")
|
originHost = strings.TrimSuffix(originHost, "]")
|
||||||
|
|
||||||
// Match configured host
|
// Match configured host
|
||||||
if host != "" && originHost == host {
|
if host != "" && originHost == host {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Match request's own Host header (same-origin)
|
// Match request's own Host header (same-origin)
|
||||||
if reqHost != "" && originHost == reqHost {
|
if reqHost != "" && originHost == reqHost {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Localhost variants
|
// Localhost variants
|
||||||
if originHost == "localhost" || originHost == "127.0.0.1" || originHost == "::1" {
|
if originHost == "localhost" || originHost == "127.0.0.1" || originHost == "::1" {
|
||||||
if port == "" || originPort == "" || originPort == port {
|
if port == "" || originPort == "" || originPort == port {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,28 +2,28 @@ package permissions
|
|||||||
|
|
||||||
// Permission bitflags.
|
// Permission bitflags.
|
||||||
const (
|
const (
|
||||||
VIEW_CHANNEL int64 = 1 << 0
|
VIEW_CHANNEL int64 = 1 << 0
|
||||||
SEND_MESSAGES int64 = 1 << 1
|
SEND_MESSAGES int64 = 1 << 1
|
||||||
MANAGE_MESSAGES int64 = 1 << 2
|
MANAGE_MESSAGES int64 = 1 << 2
|
||||||
KICK_MEMBERS int64 = 1 << 3
|
KICK_MEMBERS int64 = 1 << 3
|
||||||
BAN_MEMBERS int64 = 1 << 4
|
BAN_MEMBERS int64 = 1 << 4
|
||||||
MANAGE_SERVER int64 = 1 << 5
|
MANAGE_SERVER int64 = 1 << 5
|
||||||
MANAGE_CHANNELS int64 = 1 << 6
|
MANAGE_CHANNELS int64 = 1 << 6
|
||||||
ADMINISTRATOR int64 = 1 << 7
|
ADMINISTRATOR int64 = 1 << 7
|
||||||
CONNECT_VOICE int64 = 1 << 8
|
CONNECT_VOICE int64 = 1 << 8
|
||||||
SPEAK_VOICE int64 = 1 << 9
|
SPEAK_VOICE int64 = 1 << 9
|
||||||
SHARE_SCREEN int64 = 1 << 10
|
SHARE_SCREEN int64 = 1 << 10
|
||||||
MUTE_MEMBERS int64 = 1 << 11
|
MUTE_MEMBERS int64 = 1 << 11
|
||||||
CREATE_INSTANT_INVITE int64 = 1 << 12
|
CREATE_INSTANT_INVITE int64 = 1 << 12
|
||||||
CHANGE_NICKNAME int64 = 1 << 13
|
CHANGE_NICKNAME int64 = 1 << 13
|
||||||
MANAGE_NICKNAMES int64 = 1 << 14
|
MANAGE_NICKNAMES int64 = 1 << 14
|
||||||
MANAGE_ROLES int64 = 1 << 15
|
MANAGE_ROLES int64 = 1 << 15
|
||||||
MANAGE_WEBHOOKS int64 = 1 << 16
|
MANAGE_WEBHOOKS int64 = 1 << 16
|
||||||
EMBED_LINKS int64 = 1 << 17
|
EMBED_LINKS int64 = 1 << 17
|
||||||
ATTACH_FILES int64 = 1 << 18
|
ATTACH_FILES int64 = 1 << 18
|
||||||
ADD_REACTIONS int64 = 1 << 19
|
ADD_REACTIONS int64 = 1 << 19
|
||||||
USE_EXTERNAL_EMOJIS int64 = 1 << 20
|
USE_EXTERNAL_EMOJIS int64 = 1 << 20
|
||||||
MENTION_EVERYONE int64 = 1 << 21
|
MENTION_EVERYONE int64 = 1 << 21
|
||||||
)
|
)
|
||||||
|
|
||||||
// DefaultEveryonePermissions is granted to the @everyone role when a server is created.
|
// DefaultEveryonePermissions is granted to the @everyone role when a server is created.
|
||||||
|
|||||||
@@ -183,8 +183,6 @@ func (h *Handler) SendPush(ctx context.Context, userID string, payload map[strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// SendChannelNotification sends a push notification to all server members (except the author)
|
// SendChannelNotification sends a push notification to all server members (except the author)
|
||||||
// when a new message is posted in a channel.
|
// when a new message is posted in a channel.
|
||||||
func (h *Handler) SendChannelNotification(ctx context.Context, channelID, authorID, channelName, content string) {
|
func (h *Handler) SendChannelNotification(ctx context.Context, channelID, authorID, channelName, content string) {
|
||||||
@@ -254,6 +252,7 @@ func (h *Handler) SendChannelNotification(ctx context.Context, channelID, author
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateVAPIDKeys generates a new VAPID key pair.
|
// GenerateVAPIDKeys generates a new VAPID key pair.
|
||||||
func GenerateVAPIDKeys() (publicKey, privateKey string, err error) {
|
func GenerateVAPIDKeys() (publicKey, privateKey string, err error) {
|
||||||
privateKeyBytes := make([]byte, 32)
|
privateKeyBytes := make([]byte, 32)
|
||||||
|
|||||||
@@ -148,9 +148,9 @@ func (h *Handler) Add(w http.ResponseWriter, r *http.Request) {
|
|||||||
h.hub.BroadcastToServer(serverID, gateway.Event{
|
h.hub.BroadcastToServer(serverID, gateway.Event{
|
||||||
Type: gateway.EventReactionAdd,
|
Type: gateway.EventReactionAdd,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"reaction": reaction,
|
"reaction": reaction,
|
||||||
"channel_id": channelID,
|
"channel_id": channelID,
|
||||||
"message_id": messageID,
|
"message_id": messageID,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
lksdk "github.com/livekit/server-sdk-go/v2"
|
|
||||||
"github.com/livekit/protocol/auth"
|
"github.com/livekit/protocol/auth"
|
||||||
"github.com/livekit/protocol/livekit"
|
"github.com/livekit/protocol/livekit"
|
||||||
|
lksdk "github.com/livekit/server-sdk-go/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
apiKey string
|
apiKey string
|
||||||
apiSecret string
|
apiSecret string
|
||||||
url string
|
url string
|
||||||
roomClient *lksdk.RoomServiceClient
|
roomClient *lksdk.RoomServiceClient
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,8 +62,8 @@ func (c *Client) CreateRoom(name string) (*livekit.Room, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
room, err := c.roomClient.CreateRoom(context.Background(), &livekit.CreateRoomRequest{
|
room, err := c.roomClient.CreateRoom(context.Background(), &livekit.CreateRoomRequest{
|
||||||
Name: name,
|
Name: name,
|
||||||
EmptyTimeout: 300, // 5 minutes before auto-delete when empty
|
EmptyTimeout: 300, // 5 minutes before auto-delete when empty
|
||||||
MaxParticipants: 20,
|
MaxParticipants: 20,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user