mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
chore: remaining server changes (code quality, go mod tidy)
Go mod tidy, minor server-side adjustments from security verification and code quality cleanup pass.
This commit is contained in:
@@ -1,8 +1,42 @@
|
||||
version: "2"
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- gocritic # opinionated checks for bugs, performance, style
|
||||
- gosec # security-focused static analysis (SQL injection, hardcoded creds, weak crypto)
|
||||
- errcheck # ensures all errors are checked
|
||||
- bodyclose # detects unclosed HTTP response bodies
|
||||
- contextcheck # verifies context.Context propagation
|
||||
- nilerr # detects returning nil when err is not nil
|
||||
- prealloc # suggests pre-allocating slices for performance
|
||||
- unconvert # removes unnecessary type conversions
|
||||
- unparam # finds unused function parameters
|
||||
- wastedassign # finds wasted assignments
|
||||
- staticcheck # advanced static analysis (correctness, performance, deprecation)
|
||||
|
||||
settings:
|
||||
staticcheck:
|
||||
checks:
|
||||
- "all"
|
||||
- "-SA1019" # suppress deprecated usage warnings (websocket library migration tracked separately)
|
||||
gocritic:
|
||||
enabled-tags:
|
||||
- diagnostic
|
||||
- performance
|
||||
disabled-checks:
|
||||
- hugeParam # too noisy for struct-heavy code
|
||||
gosec:
|
||||
excludes:
|
||||
- G104 # unhandled errors — errcheck covers this better
|
||||
- G304 # file path from variable — expected in file storage code
|
||||
- G706 # log injection — false positive with slog structured logging (values are typed key-value pairs, not interpolated)
|
||||
|
||||
exclusions:
|
||||
# Suppress noisy linters in test files
|
||||
rules:
|
||||
- linters: [unparam]
|
||||
path: _test\.go
|
||||
- linters: [gosec]
|
||||
path: _test\.go
|
||||
- linters: [errcheck]
|
||||
path: _test\.go
|
||||
|
||||
@@ -57,6 +57,7 @@ func NewHandler(database *db.DB, version string, hub HubBroadcaster, u *updater.
|
||||
}
|
||||
|
||||
// Handler returns the admin panel http.Handler using a nil database.
|
||||
//
|
||||
// Deprecated: use NewHandler instead. Kept for backwards-compat with any
|
||||
// caller that already imported this symbol before Phase 6.
|
||||
func Handler() http.Handler {
|
||||
|
||||
@@ -116,12 +116,12 @@ func handleDeleteBackup(database *db.DB) http.Handler {
|
||||
}
|
||||
|
||||
backupPath := filepath.Join("data", "backups", name)
|
||||
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(backupPath); os.IsNotExist(err) { //nolint:gosec // G703: path is sanitized above
|
||||
writeErr(w, http.StatusNotFound, "NOT_FOUND", "backup not found")
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Remove(backupPath); err != nil {
|
||||
if err := os.Remove(backupPath); err != nil { //nolint:gosec // G703: path is sanitized above
|
||||
writeErr(w, http.StatusInternalServerError, "INTERNAL_ERROR", "failed to delete backup")
|
||||
return
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func handleRestoreBackup(database *db.DB) http.Handler {
|
||||
}
|
||||
|
||||
backupPath := filepath.Join("data", "backups", name)
|
||||
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
|
||||
if _, err := os.Stat(backupPath); os.IsNotExist(err) { //nolint:gosec // G703: path is sanitized above
|
||||
writeErr(w, http.StatusNotFound, "NOT_FOUND", "backup not found")
|
||||
return
|
||||
}
|
||||
@@ -195,7 +195,7 @@ func handleRestoreBackup(database *db.DB) http.Handler {
|
||||
|
||||
// copyFile streams src to dst without loading the entire file into memory.
|
||||
func copyFile(src, dst string) error {
|
||||
in, err := os.Open(src)
|
||||
in, err := os.Open(src) //nolint:gosec // G703: src is from sanitized backup path
|
||||
if err != nil {
|
||||
return fmt.Errorf("open source: %w", err)
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ func handleListUsers(database *db.DB) http.HandlerFunc {
|
||||
}
|
||||
|
||||
safe := make([]adminUserResponse, len(users))
|
||||
for i, u := range users {
|
||||
safe[i] = toAdminUserResponse(u)
|
||||
for i := range users {
|
||||
safe[i] = toAdminUserResponse(users[i])
|
||||
}
|
||||
writeJSON(w, http.StatusOK, safe)
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func writeErr(w http.ResponseWriter, status int, code, msg string) {
|
||||
writeJSON(w, status, errorResponse{Error: code, Message: msg})
|
||||
}
|
||||
|
||||
func pathInt64(r *http.Request, param string) (int64, error) {
|
||||
func pathInt64(r *http.Request, param string) (int64, error) { //nolint:unparam // kept generic for future URL params
|
||||
raw := chi.URLParam(r, param)
|
||||
return strconv.ParseInt(raw, 10, 64)
|
||||
}
|
||||
|
||||
@@ -202,8 +202,8 @@ func NewMultiHandler(stdout slog.Handler, buf *RingBuffer, minLevel slog.Leveler
|
||||
}
|
||||
}
|
||||
|
||||
func (h *multiHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
return h.stdout.Enabled(context.Background(), level) || h.ring.Enabled(level)
|
||||
func (h *multiHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||
return h.stdout.Enabled(ctx, level) || h.ring.Enabled(level)
|
||||
}
|
||||
|
||||
func (h *multiHandler) Handle(ctx context.Context, r slog.Record) error {
|
||||
|
||||
@@ -401,7 +401,7 @@ func (m *mockHubWB) BroadcastChannelUpdate(ch *db.Channel) {}
|
||||
func (m *mockHubWB) BroadcastChannelDelete(channelID int64) {}
|
||||
func (m *mockHubWB) BroadcastMemberBan(userID int64) {}
|
||||
func (m *mockHubWB) BroadcastMemberUpdate(userID int64, roleName string) {}
|
||||
func (m *mockHubWB) ClientCount() int { return 0 }
|
||||
func (m *mockHubWB) ClientCount() int { return 0 }
|
||||
|
||||
// TestSpawnDetached_ValidExecutable verifies that spawnDetached can start a
|
||||
// real executable (the Go test binary itself) with a flag that causes immediate
|
||||
|
||||
@@ -133,7 +133,7 @@ func handleApplyUpdate(u *updater.Updater, hub HubBroadcaster, _ string) http.Ha
|
||||
|
||||
// spawnDetached starts a new process that is not attached to the current one.
|
||||
func spawnDetached(exePath string, args []string) error {
|
||||
cmd := exec.Command(exePath, args...)
|
||||
cmd := exec.Command(exePath, args...) //nolint:gosec // G204: command path from trusted server config
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
|
||||
@@ -21,9 +21,9 @@ type tauriPlatformResponse struct {
|
||||
|
||||
// tauriUpdateResponse is the JSON shape the Tauri updater plugin expects.
|
||||
type tauriUpdateResponse struct {
|
||||
Version string `json:"version"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
PubDate string `json:"pub_date,omitempty"`
|
||||
Version string `json:"version"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
PubDate string `json:"pub_date,omitempty"`
|
||||
Platforms map[string]tauriPlatformResponse `json:"platforms"`
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ func handleDiagnosticsConnectivity(
|
||||
clientAddr := clientIP(r)
|
||||
|
||||
lkHealthy := false
|
||||
if ok, _ := hub.LiveKitHealthCheck(); ok {
|
||||
if ok, _ := hub.LiveKitHealthCheck(); ok { //nolint:contextcheck // TODO: propagate context through this call path
|
||||
lkHealthy = true
|
||||
}
|
||||
|
||||
|
||||
@@ -35,9 +35,9 @@ type createDMRequest struct {
|
||||
|
||||
// createDMResponse is the JSON response for POST /api/v1/dms.
|
||||
type createDMResponse struct {
|
||||
ChannelID int64 `json:"channel_id"`
|
||||
ChannelID int64 `json:"channel_id"`
|
||||
Recipient db.DMUser `json:"recipient"`
|
||||
Created bool `json:"created"`
|
||||
Created bool `json:"created"`
|
||||
}
|
||||
|
||||
// listDMsResponse is the JSON response for GET /api/v1/dms.
|
||||
@@ -102,7 +102,7 @@ func handleCreateDM(database *db.DB) http.HandlerFunc {
|
||||
}
|
||||
|
||||
// Get or create the DM channel.
|
||||
ch, created, err := database.GetOrCreateDMChannel(user.ID, req.RecipientID)
|
||||
ch, created, err := database.GetOrCreateDMChannel(user.ID, req.RecipientID) //nolint:contextcheck // TODO: propagate context through this call path
|
||||
if err != nil {
|
||||
slog.Error("handleCreateDM GetOrCreateDMChannel", "err", err,
|
||||
"user_id", user.ID, "recipient_id", req.RecipientID)
|
||||
|
||||
@@ -129,9 +129,12 @@ func proxyWebSocket(w http.ResponseWriter, r *http.Request, target *url.URL, all
|
||||
backendURL.RawQuery = r.URL.RawQuery
|
||||
|
||||
// Connect to LiveKit backend.
|
||||
backConn, _, err := websocket.Dial(r.Context(), backendURL.String(), &websocket.DialOptions{
|
||||
backConn, dialResp, err := websocket.Dial(r.Context(), backendURL.String(), &websocket.DialOptions{
|
||||
Subprotocols: r.Header.Values("Sec-WebSocket-Protocol"),
|
||||
})
|
||||
if dialResp != nil && dialResp.Body != nil {
|
||||
defer dialResp.Body.Close() //nolint:errcheck // best-effort close
|
||||
}
|
||||
if err != nil {
|
||||
slog.Warn("livekit proxy: backend dial failed", "host", backendURL.Host, "path", backendURL.Path, "err", err)
|
||||
writeJSON(w, http.StatusBadGateway, errorResponse{
|
||||
@@ -188,11 +191,11 @@ func copyWS(ctx context.Context, dst, src *websocket.Conn) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err = io.Copy(writer, reader); err != nil {
|
||||
return err
|
||||
if _, copyErr := io.Copy(writer, reader); copyErr != nil {
|
||||
return copyErr
|
||||
}
|
||||
if err = writer.Close(); err != nil {
|
||||
return err
|
||||
if closeErr := writer.Close(); closeErr != nil {
|
||||
return closeErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
bcryptCost = 12
|
||||
minPassLen = 8
|
||||
maxPassLen = 72 // bcrypt silently truncates beyond 72 bytes
|
||||
bcryptCost = 12
|
||||
minPassLen = 8
|
||||
maxPassLen = 72 // bcrypt silently truncates beyond 72 bytes
|
||||
)
|
||||
|
||||
// ErrPasswordTooShort is returned when the password is below the minimum length.
|
||||
|
||||
+13
-13
@@ -34,12 +34,12 @@ type GitHubConfig struct {
|
||||
|
||||
// VoiceConfig holds LiveKit server connection and voice quality settings.
|
||||
type VoiceConfig struct {
|
||||
LiveKitAPIKey string `koanf:"livekit_api_key"` // LiveKit API key
|
||||
LiveKitAPISecret string `koanf:"livekit_api_secret"` // LiveKit API secret
|
||||
LiveKitURL string `koanf:"livekit_url"` // LiveKit server WebSocket URL (e.g. ws://localhost:7880)
|
||||
LiveKitBinaryPath string `koanf:"livekit_binary"` // path to livekit-server binary; empty = don't auto-start
|
||||
NodeIP string `koanf:"node_ip"` // public IP for WebRTC ICE candidates; empty = auto-detect
|
||||
Quality string `koanf:"quality"` // low | medium | high
|
||||
LiveKitAPIKey string `koanf:"livekit_api_key"` // LiveKit API key
|
||||
LiveKitAPISecret string `koanf:"livekit_api_secret"` // LiveKit API secret
|
||||
LiveKitURL string `koanf:"livekit_url"` // LiveKit server WebSocket URL (e.g. ws://localhost:7880)
|
||||
LiveKitBinaryPath string `koanf:"livekit_binary"` // path to livekit-server binary; empty = don't auto-start
|
||||
NodeIP string `koanf:"node_ip"` // public IP for WebRTC ICE candidates; empty = auto-detect
|
||||
Quality string `koanf:"quality"` // low | medium | high
|
||||
}
|
||||
|
||||
// ServerConfig holds HTTP server settings.
|
||||
@@ -82,12 +82,12 @@ func defaults() Config {
|
||||
AllowedOrigins: []string{},
|
||||
TrustedProxies: []string{},
|
||||
AdminAllowedCIDRs: []string{
|
||||
"127.0.0.0/8", // localhost IPv4
|
||||
"::1/128", // localhost IPv6
|
||||
"10.0.0.0/8", // private class A
|
||||
"172.16.0.0/12", // private class B
|
||||
"192.168.0.0/16", // private class C
|
||||
"fc00::/7", // IPv6 unique local
|
||||
"127.0.0.0/8", // localhost IPv4
|
||||
"::1/128", // localhost IPv6
|
||||
"10.0.0.0/8", // private class A
|
||||
"172.16.0.0/12", // private class B
|
||||
"192.168.0.0/16", // private class C
|
||||
"fc00::/7", // IPv6 unique local
|
||||
},
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
@@ -229,7 +229,7 @@ func Load(cfgPath string) (*Config, error) {
|
||||
// production — NewLiveKitClient rejects them.
|
||||
const (
|
||||
DefaultLiveKitAPIKey = "devkey"
|
||||
DefaultLiveKitAPISecret = "owncord-dev-secret-key-min-32chars"
|
||||
DefaultLiveKitAPISecret = "owncord-dev-secret-key-min-32chars" //nolint:gosec // G101: false positive — config key name, not a credential
|
||||
)
|
||||
|
||||
// IsDefaultVoiceCredentials returns true when the voice config still uses
|
||||
|
||||
@@ -64,7 +64,7 @@ func (d *DB) LinkAttachmentsToMessage(messageID int64, attachmentIDs []string) (
|
||||
args = append(args, id)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(
|
||||
query := fmt.Sprintf( //nolint:gosec // G201: placeholder interpolation, not user input
|
||||
`UPDATE attachments SET message_id = ? WHERE id IN (%s) AND message_id IS NULL`,
|
||||
strings.Join(placeholders, ","),
|
||||
)
|
||||
@@ -88,7 +88,7 @@ func (d *DB) GetAttachmentsByMessageIDs(msgIDs []int64) (map[int64][]AttachmentI
|
||||
args[i] = id
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(
|
||||
query := fmt.Sprintf( //nolint:gosec // G201: placeholder interpolation, not user input
|
||||
`SELECT id, message_id, filename, size, mime_type, width, height
|
||||
FROM attachments WHERE message_id IN (%s)`,
|
||||
strings.Join(placeholders, ","),
|
||||
|
||||
@@ -219,7 +219,7 @@ func (d *DB) GetChannelTypes(ids []int64) (map[int64]string, error) {
|
||||
args[i] = id
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(
|
||||
query := fmt.Sprintf( //nolint:gosec // G201: placeholder interpolation, not user input
|
||||
`SELECT id, type FROM channels WHERE id IN (%s)`,
|
||||
strings.Join(placeholders, ","),
|
||||
)
|
||||
|
||||
@@ -11,12 +11,12 @@ import (
|
||||
|
||||
// DMChannelInfo holds a DM channel summary for the channel list.
|
||||
type DMChannelInfo struct {
|
||||
ChannelID int64 `json:"channel_id"`
|
||||
Recipient DMUser `json:"recipient"`
|
||||
LastMessageID *int64 `json:"last_message_id"`
|
||||
LastMessage string `json:"last_message"`
|
||||
LastMessageAt string `json:"last_message_at"`
|
||||
UnreadCount int `json:"unread_count"`
|
||||
ChannelID int64 `json:"channel_id"`
|
||||
Recipient DMUser `json:"recipient"`
|
||||
LastMessageID *int64 `json:"last_message_id"`
|
||||
LastMessage string `json:"last_message"`
|
||||
LastMessageAt string `json:"last_message_at"`
|
||||
UnreadCount int `json:"unread_count"`
|
||||
}
|
||||
|
||||
// DMUser is the public-facing shape for a DM participant.
|
||||
|
||||
@@ -383,7 +383,7 @@ func (d *DB) getReactionsBatch(msgIDs []int64, requestingUserID int64) (map[int6
|
||||
placeholders := sb.String()
|
||||
|
||||
// Query: aggregate count + check if requesting user reacted.
|
||||
query := fmt.Sprintf(
|
||||
query := fmt.Sprintf( //nolint:gosec // G201: placeholder interpolation, not user input
|
||||
`SELECT r.message_id, r.emoji, COUNT(*) as cnt,
|
||||
MAX(CASE WHEN r.user_id = ? THEN 1 ELSE 0 END) as me
|
||||
FROM reactions r
|
||||
|
||||
+13
-15
@@ -18,6 +18,7 @@ package db
|
||||
// filename without executing the SQL, so subsequent runs treat them as done.
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"sort"
|
||||
@@ -46,8 +47,10 @@ func isExistingDatabase(d *DB) (bool, error) {
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='users'",
|
||||
).Scan(&name)
|
||||
if err != nil {
|
||||
// sql.ErrNoRows means the table does not exist.
|
||||
return false, nil
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("isExistingDatabase: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -59,7 +62,10 @@ func schemaVersionsExists(d *DB) (bool, error) {
|
||||
"SELECT name FROM sqlite_master WHERE type='table' AND name='schema_versions'",
|
||||
).Scan(&name)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("schemaVersionsExists: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -71,22 +77,14 @@ func isApplied(d *DB, filename string) (bool, error) {
|
||||
"SELECT version FROM schema_versions WHERE version = ?", filename,
|
||||
).Scan(&v)
|
||||
if err != nil {
|
||||
return false, nil
|
||||
if err == sql.ErrNoRows {
|
||||
return false, nil
|
||||
}
|
||||
return false, fmt.Errorf("isApplied: %w", err)
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// recordApplied inserts a migration filename into schema_versions.
|
||||
func recordApplied(d *DB, filename string) error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
"INSERT INTO schema_versions (version) VALUES (?)", filename,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("recording migration %s: %w", filename, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// sqlFilenames returns all .sql entries from the FS sorted lexicographically.
|
||||
func sqlFilenames(fsys fs.FS) ([]string, error) {
|
||||
entries, err := fs.ReadDir(fsys, ".")
|
||||
|
||||
+1
-2
@@ -69,7 +69,7 @@ func run(log *slog.Logger, logBuf *admin.RingBuffer) error {
|
||||
}
|
||||
|
||||
// ── 2. Ensure data directory exists ────────────────────────────────────
|
||||
if mkdirErr := os.MkdirAll(cfg.Server.DataDir, 0o755); mkdirErr != nil {
|
||||
if mkdirErr := os.MkdirAll(cfg.Server.DataDir, 0o750); mkdirErr != nil {
|
||||
return fmt.Errorf("creating data dir %s: %w", cfg.Server.DataDir, mkdirErr)
|
||||
}
|
||||
|
||||
@@ -333,4 +333,3 @@ func getOutboundIP() string {
|
||||
addr := conn.LocalAddr().(*net.UDPAddr)
|
||||
return addr.IP.String()
|
||||
}
|
||||
|
||||
|
||||
+20
-9
@@ -140,30 +140,40 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatalf("failed to open database at %s: %v", *dbPath, err)
|
||||
}
|
||||
defer func() { _ = database.Close() }()
|
||||
|
||||
exitCode := run(database)
|
||||
_ = database.Close()
|
||||
os.Exit(exitCode)
|
||||
}
|
||||
|
||||
func run(database *db.DB) int {
|
||||
if err := db.Migrate(database); err != nil {
|
||||
log.Fatalf("failed to run migrations: %v", err)
|
||||
log.Printf("failed to run migrations: %v", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
userIDs, err := createUsers(database)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create users: %v", err)
|
||||
log.Printf("failed to create users: %v", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
channelIDs, err := createChannels(database)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create channels: %v", err)
|
||||
log.Printf("failed to create channels: %v", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
msgCount, err := createMessages(database, channelIDs, userIDs)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create messages: %v", err)
|
||||
log.Printf("failed to create messages: %v", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
dmMsgCount, err := createDMConversation(database, userIDs)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create DM conversation: %v", err)
|
||||
log.Printf("failed to create DM conversation: %v", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
fmt.Println("--- Seed complete ---")
|
||||
@@ -171,6 +181,7 @@ func main() {
|
||||
fmt.Printf(" Channels: %d\n", len(channelIDs))
|
||||
fmt.Printf(" Messages: %d (channel) + %d (DM) = %d total\n",
|
||||
msgCount, dmMsgCount, msgCount+dmMsgCount)
|
||||
return 0
|
||||
}
|
||||
|
||||
// ─── User creation ──────────────────────────────────────────────────────────
|
||||
@@ -234,8 +245,8 @@ func createChannels(database *db.DB) ([]int64, error) {
|
||||
return nil, fmt.Errorf("listing channels: %w", err)
|
||||
}
|
||||
existingByName := make(map[string]int64, len(existing))
|
||||
for _, ch := range existing {
|
||||
existingByName[ch.Name] = ch.ID
|
||||
for i := range existing {
|
||||
existingByName[existing[i].Name] = existing[i].ID
|
||||
}
|
||||
|
||||
for i, sc := range seedChannels {
|
||||
@@ -353,7 +364,7 @@ func createDMConversation(database *db.DB, userIDs []int64) (int, error) {
|
||||
func init() {
|
||||
// The default DB path is data/chatserver.db. Ensure the data directory
|
||||
// exists so db.Open doesn't fail on a fresh checkout.
|
||||
if err := os.MkdirAll("data", 0o755); err != nil {
|
||||
if err := os.MkdirAll("data", 0o750); err != nil {
|
||||
log.Printf("warning: could not create data directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ var blockedMagic = []struct {
|
||||
name string
|
||||
magic []byte
|
||||
}{
|
||||
{"PE executable", []byte("MZ")}, // Windows .exe / .dll
|
||||
{"ELF binary", []byte("\x7fELF")}, // Linux binaries
|
||||
{"Mach-O 64", []byte("\xcf\xfa\xed\xfe")}, // macOS 64-bit
|
||||
{"Mach-O 32", []byte("\xce\xfa\xed\xfe")}, // macOS 32-bit
|
||||
{"shell script", []byte("#!")}, // Shebang scripts (.sh, .py, etc.)
|
||||
{"PE executable", []byte("MZ")}, // Windows .exe / .dll
|
||||
{"ELF binary", []byte("\x7fELF")}, // Linux binaries
|
||||
{"Mach-O 64", []byte("\xcf\xfa\xed\xfe")}, // macOS 64-bit
|
||||
{"Mach-O 32", []byte("\xce\xfa\xed\xfe")}, // macOS 32-bit
|
||||
{"shell script", []byte("#!")}, // Shebang scripts (.sh, .py, etc.)
|
||||
}
|
||||
|
||||
// ValidateFileType checks the first few bytes of a file against known blocked
|
||||
@@ -45,7 +45,7 @@ type Storage struct {
|
||||
// New creates a Storage instance that stores files in dir.
|
||||
// dir is created if it does not exist.
|
||||
func New(dir string, maxSizeMB int) (*Storage, error) {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
if err := os.MkdirAll(dir, 0o750); err != nil {
|
||||
return nil, fmt.Errorf("creating storage dir %s: %w", dir, err)
|
||||
}
|
||||
return &Storage{dir: dir, maxSizeMB: maxSizeMB}, nil
|
||||
|
||||
+12
-12
@@ -21,22 +21,22 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBaseURL = "https://api.github.com"
|
||||
cacheTTL = 1 * time.Hour
|
||||
errorCacheTTL = 5 * time.Minute
|
||||
binaryAsset = "chatserver.exe"
|
||||
checksumAsset = "checksums.sha256"
|
||||
defaultBaseURL = "https://api.github.com"
|
||||
cacheTTL = 1 * time.Hour
|
||||
errorCacheTTL = 5 * time.Minute
|
||||
binaryAsset = "chatserver.exe"
|
||||
checksumAsset = "checksums.sha256"
|
||||
)
|
||||
|
||||
// UpdateInfo holds the result of a version check.
|
||||
type UpdateInfo struct {
|
||||
Current string `json:"current"`
|
||||
Latest string `json:"latest"`
|
||||
UpdateAvailable bool `json:"update_available"`
|
||||
ReleaseURL string `json:"release_url"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
ChecksumURL string `json:"checksum_url"`
|
||||
ReleaseNotes string `json:"release_notes"`
|
||||
Current string `json:"current"`
|
||||
Latest string `json:"latest"`
|
||||
UpdateAvailable bool `json:"update_available"`
|
||||
ReleaseURL string `json:"release_url"`
|
||||
DownloadURL string `json:"download_url"`
|
||||
ChecksumURL string `json:"checksum_url"`
|
||||
ReleaseNotes string `json:"release_notes"`
|
||||
Assets []Asset `json:"assets,omitempty"`
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ type chatSendPayload struct {
|
||||
}
|
||||
|
||||
// handleChatSend processes a chat_send message.
|
||||
func (h *Hub) handleChatSend(ctx context.Context, c *Client, reqID string, payload json.RawMessage) {
|
||||
func (h *Hub) handleChatSend(_ context.Context, c *Client, reqID string, payload json.RawMessage) {
|
||||
ratKey := fmt.Sprintf("chat:%d", c.userID)
|
||||
if !h.limiter.Allow(ratKey, chatRateLimit, chatWindow) {
|
||||
c.sendMsg(buildRateLimitError("too many messages", chatWindow.Seconds()))
|
||||
@@ -229,7 +229,7 @@ func (h *Hub) broadcastChatMessage(c *Client, channelID int64, isDM bool, broadc
|
||||
}
|
||||
|
||||
// handleChatEdit processes a chat_edit message.
|
||||
func (h *Hub) handleChatEdit(ctx context.Context, c *Client, _ string, payload json.RawMessage) {
|
||||
func (h *Hub) handleChatEdit(_ context.Context, c *Client, _ string, payload json.RawMessage) {
|
||||
ratKey := fmt.Sprintf("chat_edit:%d", c.userID)
|
||||
if !h.limiter.Allow(ratKey, chatRateLimit, chatWindow) {
|
||||
c.sendMsg(buildRateLimitError("too many edits", chatWindow.Seconds()))
|
||||
@@ -278,12 +278,10 @@ func (h *Hub) handleChatEdit(ctx context.Context, c *Client, _ string, payload j
|
||||
c.sendMsg(buildErrorMsg(ErrCodeForbidden, "cannot edit this message"))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
} else if !h.hasChannelPerm(c, msg.ChannelID, permissions.SendMessages) {
|
||||
// Re-check that the user still has SendMessages permission on this channel.
|
||||
if !h.hasChannelPerm(c, msg.ChannelID, permissions.SendMessages) {
|
||||
c.sendMsg(buildErrorMsg(ErrCodeForbidden, "cannot edit this message"))
|
||||
return
|
||||
}
|
||||
c.sendMsg(buildErrorMsg(ErrCodeForbidden, "cannot edit this message"))
|
||||
return
|
||||
}
|
||||
|
||||
// EditMessage checks ownership internally.
|
||||
@@ -315,7 +313,7 @@ func (h *Hub) handleChatEdit(ctx context.Context, c *Client, _ string, payload j
|
||||
}
|
||||
|
||||
// handleChatDelete processes a chat_delete message.
|
||||
func (h *Hub) handleChatDelete(ctx context.Context, c *Client, _ string, payload json.RawMessage) {
|
||||
func (h *Hub) handleChatDelete(_ context.Context, c *Client, _ string, payload json.RawMessage) {
|
||||
ratKey := fmt.Sprintf("chat_delete:%d", c.userID)
|
||||
if !h.limiter.Allow(ratKey, chatRateLimit, chatWindow) {
|
||||
c.sendMsg(buildRateLimitError("too many deletes", chatWindow.Seconds()))
|
||||
|
||||
@@ -23,7 +23,7 @@ func registerPresenceHandlers(r *HandlerRegistry) {
|
||||
}
|
||||
|
||||
// handleTyping processes a typing_start message.
|
||||
func (h *Hub) handleTyping(ctx context.Context, c *Client, payload json.RawMessage) {
|
||||
func (h *Hub) handleTyping(_ context.Context, c *Client, payload json.RawMessage) {
|
||||
channelID, err := parseChannelID(payload)
|
||||
if err != nil || channelID <= 0 {
|
||||
c.sendMsg(buildErrorMsg(ErrCodeBadRequest, "channel_id must be positive integer"))
|
||||
@@ -45,10 +45,8 @@ func (h *Hub) handleTyping(ctx context.Context, c *Client, payload json.RawMessa
|
||||
if dmErr != nil || !ok {
|
||||
return // silently drop — not a DM participant
|
||||
}
|
||||
} else {
|
||||
if !h.hasChannelPerm(c, channelID, permissions.ReadMessages) {
|
||||
return // silently drop — no read permission on this channel
|
||||
}
|
||||
} else if !h.hasChannelPerm(c, channelID, permissions.ReadMessages) {
|
||||
return // silently drop — no read permission on this channel
|
||||
}
|
||||
|
||||
var username string
|
||||
@@ -65,7 +63,7 @@ func (h *Hub) handleTyping(ctx context.Context, c *Client, payload json.RawMessa
|
||||
}
|
||||
|
||||
// handlePresence processes a presence_update message.
|
||||
func (h *Hub) handlePresence(ctx context.Context, c *Client, payload json.RawMessage) {
|
||||
func (h *Hub) handlePresence(_ context.Context, c *Client, payload json.RawMessage) {
|
||||
ratKey := fmt.Sprintf("presence:%d", c.userID)
|
||||
if !h.limiter.Allow(ratKey, presenceRateLimit, presenceWindow) {
|
||||
c.sendMsg(buildRateLimitError("too many presence updates", presenceWindow.Seconds()))
|
||||
@@ -97,7 +95,7 @@ func (h *Hub) handlePresence(ctx context.Context, c *Client, payload json.RawMes
|
||||
// handleChannelFocus sets which channel the client is currently viewing,
|
||||
// so channel-scoped broadcasts (chat messages, typing) reach them.
|
||||
// Also updates read_states so unread counts decrease when the user views a channel.
|
||||
func (h *Hub) handleChannelFocus(ctx context.Context, c *Client, payload json.RawMessage) {
|
||||
func (h *Hub) handleChannelFocus(_ context.Context, c *Client, payload json.RawMessage) {
|
||||
chID, err := parseChannelID(payload)
|
||||
if err != nil || chID <= 0 {
|
||||
slog.Debug("handleChannelFocus: invalid channel_id", "user_id", c.userID, "err", err)
|
||||
@@ -116,10 +114,8 @@ func (h *Hub) handleChannelFocus(ctx context.Context, c *Client, payload json.Ra
|
||||
c.sendMsg(buildErrorMsg(ErrCodeForbidden, "not a participant in this DM"))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !h.requireChannelPerm(c, chID, permissions.ReadMessages, "READ_MESSAGES") {
|
||||
return
|
||||
}
|
||||
} else if !h.requireChannelPerm(c, chID, permissions.ReadMessages, "READ_MESSAGES") {
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
|
||||
@@ -20,7 +20,7 @@ func registerReactionHandlers(r *HandlerRegistry) {
|
||||
}
|
||||
|
||||
// handleReaction processes reaction_add and reaction_remove messages.
|
||||
func (h *Hub) handleReaction(ctx context.Context, c *Client, add bool, payload json.RawMessage) {
|
||||
func (h *Hub) handleReaction(_ context.Context, c *Client, add bool, payload json.RawMessage) {
|
||||
ratKey := fmt.Sprintf("reaction:%d", c.userID)
|
||||
if !h.limiter.Allow(ratKey, reactionRateLimit, reactionWindow) {
|
||||
c.sendMsg(buildRateLimitError("too many reactions", reactionWindow.Seconds()))
|
||||
@@ -79,10 +79,8 @@ func (h *Hub) handleReaction(ctx context.Context, c *Client, add bool, payload j
|
||||
c.sendMsg(buildErrorMsg(ErrCodeBadRequest, "reaction failed"))
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !h.requireChannelPerm(c, msg.ChannelID, permissions.AddReactions, "ADD_REACTIONS") {
|
||||
return
|
||||
}
|
||||
} else if !h.requireChannelPerm(c, msg.ChannelID, permissions.AddReactions, "ADD_REACTIONS") {
|
||||
return
|
||||
}
|
||||
|
||||
action := "add"
|
||||
|
||||
@@ -76,7 +76,7 @@ func (p *LiveKitProcess) generateConfig() (string, error) {
|
||||
return "", fmt.Errorf("node_ip contains unsafe character %q", string(ch))
|
||||
}
|
||||
}
|
||||
nodeIPLine = fmt.Sprintf("\n node_ip: \"%s\"", p.cfg.NodeIP)
|
||||
nodeIPLine = fmt.Sprintf("\n node_ip: %q", p.cfg.NodeIP)
|
||||
}
|
||||
|
||||
content := fmt.Sprintf(`# Auto-generated by OwnCord — do not edit manually.
|
||||
@@ -98,7 +98,7 @@ logging:
|
||||
level: info
|
||||
`, nodeIPLine, p.cfg.LiveKitAPIKey, p.cfg.LiveKitAPISecret)
|
||||
|
||||
if err := os.MkdirAll(p.dataDir, 0o755); err != nil {
|
||||
if err := os.MkdirAll(p.dataDir, 0o750); err != nil {
|
||||
return "", fmt.Errorf("creating data dir: %w", err)
|
||||
}
|
||||
if err := os.WriteFile(cfgPath, []byte(content), 0o600); err != nil {
|
||||
@@ -150,10 +150,10 @@ func (p *LiveKitProcess) runLoop(ctx context.Context, cfgPath string) {
|
||||
}()
|
||||
|
||||
const (
|
||||
baseDelay = 3 * time.Second
|
||||
maxDelay = 60 * time.Second
|
||||
maxRetries = 10
|
||||
stableAfter = 30 * time.Second // reset counter if process runs longer than this
|
||||
baseDelay = 3 * time.Second
|
||||
maxDelay = 60 * time.Second
|
||||
maxRetries = 10
|
||||
stableAfter = 30 * time.Second // reset counter if process runs longer than this
|
||||
)
|
||||
|
||||
rapidFailures := 0
|
||||
@@ -164,7 +164,7 @@ func (p *LiveKitProcess) runLoop(ctx context.Context, cfgPath string) {
|
||||
return
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, p.cfg.LiveKitBinaryPath, "--config", cfgPath)
|
||||
cmd := exec.CommandContext(ctx, p.cfg.LiveKitBinaryPath, "--config", cfgPath) //nolint:gosec // G204: binary path from trusted server config
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.WaitDelay = 6 * time.Second // bound Wait to prevent goroutine leak on Windows
|
||||
|
||||
@@ -190,33 +190,29 @@ func (h *Hub) handleWebhookParticipantLeft(event *livekit.WebhookEvent) {
|
||||
slog.Info("livekit webhook: cleaned up stale voice state",
|
||||
"user_id", userID,
|
||||
"channel_id", channelID)
|
||||
} else {
|
||||
} else if h.db != nil {
|
||||
// Client has voiceChID=0 or moved to a different channel (e.g.
|
||||
// after F5 reload), or this webhook is for an older join instance.
|
||||
if h.db != nil {
|
||||
deleted, dbErr := h.db.LeaveVoiceChannelIfMatch(userID, channelID, joinToken)
|
||||
if dbErr != nil {
|
||||
slog.Error("livekit webhook: LeaveVoiceChannelIfMatch failed (stale DB row)",
|
||||
"error", dbErr, "user_id", userID, "channel_id", channelID)
|
||||
} else if deleted {
|
||||
h.BroadcastToAll(buildVoiceLeave(channelID, userID))
|
||||
slog.Info("livekit webhook: cleaned stale DB voice row after reconnect",
|
||||
"user_id", userID, "channel_id", channelID)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Client already disconnected from WS — use channel-conditional delete
|
||||
// to avoid wiping a newer row if the user reconnected and rejoined.
|
||||
if h.db != nil {
|
||||
deleted, dbErr := h.db.LeaveVoiceChannelIfMatch(userID, channelID, joinToken)
|
||||
if dbErr != nil {
|
||||
slog.Error("livekit webhook: LeaveVoiceChannelIfMatch failed (client gone)",
|
||||
slog.Error("livekit webhook: LeaveVoiceChannelIfMatch failed (stale DB row)",
|
||||
"error", dbErr, "user_id", userID, "channel_id", channelID)
|
||||
} else if deleted {
|
||||
h.BroadcastToAll(buildVoiceLeave(channelID, userID))
|
||||
slog.Info("livekit webhook: cleaned stale DB voice row after reconnect",
|
||||
"user_id", userID, "channel_id", channelID)
|
||||
}
|
||||
}
|
||||
} else if h.db != nil {
|
||||
// Client already disconnected from WS — use channel-conditional delete
|
||||
// to avoid wiping a newer row if the user reconnected and rejoined.
|
||||
deleted, dbErr := h.db.LeaveVoiceChannelIfMatch(userID, channelID, joinToken)
|
||||
if dbErr != nil {
|
||||
slog.Error("livekit webhook: LeaveVoiceChannelIfMatch failed (client gone)",
|
||||
"error", dbErr, "user_id", userID, "channel_id", channelID)
|
||||
} else if deleted {
|
||||
h.BroadcastToAll(buildVoiceLeave(channelID, userID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ const (
|
||||
MsgTypeVoiceCamera = "voice_camera"
|
||||
MsgTypeVoiceScreenshare = "voice_screenshare"
|
||||
MsgTypePing = "ping"
|
||||
MsgTypeVoiceTokenRefresh = "voice_token_refresh"
|
||||
MsgTypeVoiceTokenRefresh = "voice_token_refresh" //nolint:gosec // G101: false positive — message type constant, not a credential
|
||||
)
|
||||
|
||||
// Server → Client message types (sent in broadcasts/responses).
|
||||
|
||||
+11
-11
@@ -43,15 +43,15 @@ type memberJoinPayload struct {
|
||||
}
|
||||
|
||||
type chatMessagePayload struct {
|
||||
ID int64 `json:"id"`
|
||||
ChannelID int64 `json:"channel_id"`
|
||||
User memberUserPayload `json:"user"`
|
||||
Content string `json:"content"`
|
||||
ReplyTo *int64 `json:"reply_to"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Attachments []map[string]any `json:"attachments"`
|
||||
Reactions []any `json:"reactions"`
|
||||
Pinned bool `json:"pinned"`
|
||||
ID int64 `json:"id"`
|
||||
ChannelID int64 `json:"channel_id"`
|
||||
User memberUserPayload `json:"user"`
|
||||
Content string `json:"content"`
|
||||
ReplyTo *int64 `json:"reply_to"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Attachments []map[string]any `json:"attachments"`
|
||||
Reactions []any `json:"reactions"`
|
||||
Pinned bool `json:"pinned"`
|
||||
}
|
||||
|
||||
type memberUpdatePayload struct {
|
||||
@@ -147,7 +147,7 @@ type serverRestartPayload struct {
|
||||
|
||||
// dmChannelOpenPayload is sent when a DM is opened/reopened for a user.
|
||||
type dmChannelOpenPayload struct {
|
||||
ChannelID int64 `json:"channel_id"`
|
||||
ChannelID int64 `json:"channel_id"`
|
||||
Recipient dmUserPayload `json:"recipient"`
|
||||
}
|
||||
|
||||
@@ -370,7 +370,7 @@ func buildVoiceConfig(channelID int64, quality string, bitrate int, maxUsers int
|
||||
// buildVoiceToken constructs a voice_token message with a LiveKit token and URL.
|
||||
// url is the proxy path ("/livekit") for remote clients; direct_url is the raw
|
||||
// LiveKit URL (e.g. "ws://localhost:7880") for localhost clients.
|
||||
func buildVoiceToken(channelID int64, token string, proxyPath string, directURL string) []byte {
|
||||
func buildVoiceToken(channelID int64, token string, proxyPath string, directURL string) []byte { //nolint:unparam // kept configurable for proxy path flexibility
|
||||
return buildJSON(wsMsg{
|
||||
Type: MsgTypeVoiceToken,
|
||||
Payload: voiceTokenPayload{
|
||||
|
||||
+19
-17
@@ -16,9 +16,11 @@ import (
|
||||
"github.com/owncord/server/permissions"
|
||||
)
|
||||
|
||||
const authDeadline = 10 * time.Second
|
||||
const writeTimeout = 10 * time.Second
|
||||
const settingsCacheTTL = 30 * time.Second
|
||||
const (
|
||||
authDeadline = 10 * time.Second
|
||||
writeTimeout = 10 * time.Second
|
||||
settingsCacheTTL = 30 * time.Second
|
||||
)
|
||||
|
||||
// ServeWS upgrades an HTTP connection to WebSocket, performs in-band auth,
|
||||
// then drives the client's read/write loops.
|
||||
@@ -77,7 +79,7 @@ func ServeWS(hub *Hub, database *db.DB, allowedOrigins []string) http.HandlerFun
|
||||
func (h *Hub) upgradeAndAuth(
|
||||
conn *websocket.Conn, database *db.DB, r *http.Request,
|
||||
) (*Client, uint64, error) {
|
||||
user, tokenHash, lastSeq, err := authenticateConn(conn, database)
|
||||
user, tokenHash, lastSeq, err := authenticateConn(conn, database) //nolint:contextcheck // TODO: propagate context through this call path
|
||||
if err != nil {
|
||||
slog.Warn("ws auth failed", "err", err, "remote", r.RemoteAddr)
|
||||
_ = conn.Close(websocket.StatusPolicyViolation, "authentication failed")
|
||||
@@ -150,7 +152,7 @@ func (h *Hub) handleFreshConnect(
|
||||
}
|
||||
h.BroadcastToAll(buildVoiceLeave(vs.ChannelID, c.userID))
|
||||
if h.livekit != nil {
|
||||
go h.livekit.RemoveParticipant(vs.ChannelID, c.userID, vs.JoinedAt)
|
||||
go h.livekit.RemoveParticipant(vs.ChannelID, c.userID, vs.JoinedAt) //nolint:errcheck,gosec,contextcheck // fire-and-forget cleanup on disconnect; G118: no request-scoped context available for background goroutine
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,16 +384,16 @@ func (h *Hub) buildReady(database *db.DB, userID int64, role *db.Role) ([]byte,
|
||||
}
|
||||
}
|
||||
var visibleChannels []db.Channel
|
||||
for _, ch := range channels {
|
||||
for i := range channels {
|
||||
// When role is unavailable, include all channels (backwards compat).
|
||||
if role == nil || permissions.HasAdmin(role.Permissions) {
|
||||
visibleChannels = append(visibleChannels, ch)
|
||||
visibleChannels = append(visibleChannels, channels[i])
|
||||
continue
|
||||
}
|
||||
o := overrides[ch.ID]
|
||||
o := overrides[channels[i].ID]
|
||||
effective := permissions.EffectivePerms(role.Permissions, o.Allow, o.Deny)
|
||||
if effective&permissions.ReadMessages == permissions.ReadMessages {
|
||||
visibleChannels = append(visibleChannels, ch)
|
||||
visibleChannels = append(visibleChannels, channels[i])
|
||||
}
|
||||
}
|
||||
if visibleChannels == nil {
|
||||
@@ -407,16 +409,16 @@ func (h *Hub) buildReady(database *db.DB, userID int64, role *db.Role) ([]byte,
|
||||
|
||||
// Build protocol-compliant channel objects (strip extra fields).
|
||||
channelPayloads := make([]map[string]any, 0, len(visibleChannels))
|
||||
for _, ch := range visibleChannels {
|
||||
for i := range visibleChannels {
|
||||
entry := map[string]any{
|
||||
"id": ch.ID,
|
||||
"name": ch.Name,
|
||||
"type": ch.Type,
|
||||
"category": ch.Category,
|
||||
"position": ch.Position,
|
||||
"id": visibleChannels[i].ID,
|
||||
"name": visibleChannels[i].Name,
|
||||
"type": visibleChannels[i].Type,
|
||||
"category": visibleChannels[i].Category,
|
||||
"position": visibleChannels[i].Position,
|
||||
}
|
||||
if ch.Type == "text" {
|
||||
if u, ok := unreadMap[ch.ID]; ok {
|
||||
if visibleChannels[i].Type == "text" {
|
||||
if u, ok := unreadMap[visibleChannels[i].ID]; ok {
|
||||
entry["unread_count"] = u.UnreadCount
|
||||
entry["last_message_id"] = u.LastMessageID
|
||||
} else {
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// 1. Parses muted bool.
|
||||
// 2. Updates DB.
|
||||
// 3. Broadcasts voice_state update to channel.
|
||||
func (h *Hub) handleVoiceMute(ctx context.Context, c *Client, payload json.RawMessage) {
|
||||
func (h *Hub) handleVoiceMute(_ context.Context, c *Client, payload json.RawMessage) {
|
||||
ratKey := fmt.Sprintf("voice_mute:%d", c.userID)
|
||||
if !h.limiter.Allow(ratKey, voiceMuteRateLimit, voiceMuteWindow) {
|
||||
c.sendMsg(buildRateLimitError("too many mute toggles", voiceMuteWindow.Seconds()))
|
||||
@@ -47,7 +47,7 @@ func (h *Hub) handleVoiceMute(ctx context.Context, c *Client, payload json.RawMe
|
||||
// 1. Parses deafened bool.
|
||||
// 2. Updates DB.
|
||||
// 3. Broadcasts voice_state update to channel.
|
||||
func (h *Hub) handleVoiceDeafen(ctx context.Context, c *Client, payload json.RawMessage) {
|
||||
func (h *Hub) handleVoiceDeafen(_ context.Context, c *Client, payload json.RawMessage) {
|
||||
ratKey := fmt.Sprintf("voice_deafen:%d", c.userID)
|
||||
if !h.limiter.Allow(ratKey, voiceDeafenRateLimit, voiceDeafenWindow) {
|
||||
c.sendMsg(buildRateLimitError("too many deafen toggles", voiceDeafenWindow.Seconds()))
|
||||
@@ -84,7 +84,7 @@ func (h *Hub) handleVoiceDeafen(ctx context.Context, c *Client, payload json.Raw
|
||||
// 4. Enforces MaxVideo limit via DB count (race-free).
|
||||
// 5. Updates DB.
|
||||
// 6. Broadcasts voice_state update to channel.
|
||||
func (h *Hub) handleVoiceCamera(ctx context.Context, c *Client, payload json.RawMessage) {
|
||||
func (h *Hub) handleVoiceCamera(_ context.Context, c *Client, payload json.RawMessage) {
|
||||
ratKey := fmt.Sprintf("voice_camera:%d", c.userID)
|
||||
if !h.limiter.Allow(ratKey, voiceCameraRateLimit, voiceCameraWindow) {
|
||||
c.sendMsg(buildRateLimitError("too many camera toggles", voiceCameraWindow.Seconds()))
|
||||
@@ -149,7 +149,7 @@ func (h *Hub) handleVoiceCamera(ctx context.Context, c *Client, payload json.Raw
|
||||
// 3. Parses enabled bool.
|
||||
// 4. Updates DB.
|
||||
// 5. Broadcasts voice_state update to channel.
|
||||
func (h *Hub) handleVoiceScreenshare(ctx context.Context, c *Client, payload json.RawMessage) {
|
||||
func (h *Hub) handleVoiceScreenshare(_ context.Context, c *Client, payload json.RawMessage) {
|
||||
ratKey := fmt.Sprintf("voice_screenshare:%d", c.userID)
|
||||
if !h.limiter.Allow(ratKey, voiceScreenshareRateLimit, voiceScreenshareWindow) {
|
||||
c.sendMsg(buildRateLimitError("too many screenshare toggles", voiceScreenshareWindow.Seconds()))
|
||||
|
||||
@@ -188,7 +188,7 @@ func (h *Hub) handleVoiceJoin(ctx context.Context, c *Client, payload json.RawMe
|
||||
// handleVoiceTokenRefresh generates a fresh LiveKit token for a client
|
||||
// that is already in a voice channel. This lets clients request a new token
|
||||
// (e.g. before a manual reconnect) without leaving and rejoining voice.
|
||||
func (h *Hub) handleVoiceTokenRefresh(ctx context.Context, c *Client) {
|
||||
func (h *Hub) handleVoiceTokenRefresh(_ context.Context, c *Client) {
|
||||
ratKey := fmt.Sprintf("voice_token_refresh:%d", c.userID)
|
||||
if !h.limiter.Allow(ratKey, 1, 60*time.Second) {
|
||||
c.sendMsg(buildRateLimitError("token refresh rate limited", 60))
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
// 1. Gets old voiceChID from clearVoiceChID().
|
||||
// 2. If was in voice: remove from DB (with retry), broadcast voice_leave.
|
||||
// 3. Call livekit.RemoveParticipant (ignore errors — participant may already be gone).
|
||||
func (h *Hub) handleVoiceLeave(ctx context.Context, c *Client) {
|
||||
func (h *Hub) handleVoiceLeave(_ context.Context, c *Client) {
|
||||
oldChID, oldJoinToken := c.clearVoiceState()
|
||||
if oldChID == 0 {
|
||||
slog.Debug("handleVoiceLeave no-op (already cleared)", "user_id", c.userID)
|
||||
@@ -36,7 +36,7 @@ func (h *Hub) handleVoiceLeave(ctx context.Context, c *Client) {
|
||||
|
||||
// Remove from LiveKit (best-effort).
|
||||
if h.livekit != nil {
|
||||
if err := h.livekit.RemoveParticipant(oldChID, c.userID, oldJoinToken); err != nil {
|
||||
if err := h.livekit.RemoveParticipant(oldChID, c.userID, oldJoinToken); err != nil { //nolint:contextcheck // TODO: propagate context through this call path
|
||||
slog.Warn("handleVoiceLeave RemoveParticipant failed (may already be gone)",
|
||||
"err", err, "user_id", c.userID, "channel_id", oldChID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user