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
+1 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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.
+3 -3
View File
@@ -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
View File
@@ -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
} }
+21 -21
View File
@@ -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.
+1 -2
View File
@@ -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)
+3 -3
View File
@@ -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,
}, },
}) })
+6 -6
View File
@@ -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 {