Files
OwnCord/Server/db/voice_queries.go
T
Claude c7e6702c8c refactor(server/db): delegate voice, dm, channels, admin to dbgen (D2)
voice: all reads (GetVoiceState, GetChannelVoiceStates, GetAllVoiceStates)
and writes (join/leave/mute/deafen/camera/screenshare/clear, capacity +
camera-limit atomic guards); CountChannelVoiceUsers stays raw (no query).
dm: OpenDM, CloseDM, IsDMParticipant, GetDMParticipantIDs; GetOrCreateDMChannel
(serializable tx), GetUserDMChannels (aggregate), GetDMRecipient stay raw.
channels: List/Get (shared channelFromFields mapper), Create/Update/Delete,
slow-mode/max-users setters, permission overrides get/list-for-role/upsert/
delete; ListChannelRoleOverrides + GetChannelTypes (variable IN) stay raw.
admin: UserCount, GetServerStats counts (PRAGMA stays raw), ListAllUsers,
UpdateUserRole, ForceLogoutUser, GetUserSessions, AdminUpdateChannel,
AdminDeleteChannel, LogAudit, GetAuditLog, GetSetting, SetSetting,
GetAllSettings, CountUsersWithoutTOTP; AdminCreateChannel + Backup stay raw.

Added b2i64 and strToNullPtr mapper helpers; retired the obsolete
scanChannel/nullableString. Behavior and public signatures unchanged.

Verified: go build ./...; go test ./db ./service ./permissions ./admin;
sqlc-verify.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UA17KPvqGBX3XbXYnMf1rA
2026-07-19 15:43:46 +00:00

268 lines
8.5 KiB
Go

package db
import (
"database/sql"
"errors"
"fmt"
"sync/atomic"
"time"
"github.com/owncord/server/db/dbgen"
)
// ErrChannelFull is returned when a voice channel is at capacity.
var ErrChannelFull = errors.New("voice channel is full")
var voiceJoinSeq uint64
func newVoiceJoinToken() string {
seq := atomic.AddUint64(&voiceJoinSeq, 1)
return fmt.Sprintf("%s-%020d", time.Now().UTC().Format("2006-01-02T15:04:05.000000000Z"), seq)
}
// JoinVoiceChannel inserts or replaces the user's voice state for the given
// channel. If the user is already in a different channel, the old row is
// replaced. Muted, deafened, and speaking are reset to false on join.
//
// joined_at doubles as an opaque join-instance token so stale cleanup can
// target one specific voice session even if the user later rejoins the same
// channel.
func (d *DB) JoinVoiceChannel(userID, channelID int64) error {
if err := d.q.JoinVoiceChannel(dbCtx(), dbgen.JoinVoiceChannelParams{
UserID: userID,
ChannelID: channelID,
JoinedAt: newVoiceJoinToken(),
}); err != nil {
return fmt.Errorf("JoinVoiceChannel: %w", err)
}
return nil
}
// JoinVoiceChannelIfCapacity atomically inserts a voice state only if the
// channel has fewer than maxUsers participants. Returns ErrChannelFull when
// the channel is at capacity. This prevents the TOCTOU race where two
// concurrent joins both observe capacity and both succeed.
func (d *DB) JoinVoiceChannelIfCapacity(userID, channelID int64, maxUsers int) error {
res, err := d.q.JoinVoiceChannelIfCapacity(dbCtx(), dbgen.JoinVoiceChannelIfCapacityParams{
UserID: userID,
ChannelID: channelID,
JoinedAt: newVoiceJoinToken(),
ChannelID_2: channelID,
ChannelID_3: int64(maxUsers),
})
if err != nil {
return fmt.Errorf("JoinVoiceChannelIfCapacity: %w", err)
}
n, _ := res.RowsAffected()
if n == 0 {
return ErrChannelFull
}
return nil
}
// LeaveVoiceChannel removes the user's voice state entirely.
// It is safe to call when the user is not in any voice channel.
func (d *DB) LeaveVoiceChannel(userID int64) error {
if err := d.q.LeaveVoiceChannel(dbCtx(), userID); err != nil {
return fmt.Errorf("LeaveVoiceChannel: %w", err)
}
return nil
}
// LeaveVoiceChannelIfMatch removes the user's voice state only if the row
// still points at expectedChannelID and matches the expected join token.
// Returns true if a row was deleted.
func (d *DB) LeaveVoiceChannelIfMatch(userID, expectedChannelID int64, expectedJoinedAt string) (bool, error) {
result, err := d.q.LeaveVoiceChannelIfMatch(dbCtx(), dbgen.LeaveVoiceChannelIfMatchParams{
UserID: userID,
ChannelID: expectedChannelID,
JoinedAt: expectedJoinedAt,
})
if err != nil {
return false, fmt.Errorf("LeaveVoiceChannelIfMatch: %w", err)
}
n, _ := result.RowsAffected()
return n > 0, nil
}
// GetVoiceState returns the current voice state for the given user,
// or nil if the user is not in any voice channel.
func (d *DB) GetVoiceState(userID int64) (*VoiceState, error) {
r, err := d.q.GetUserVoiceState(dbCtx(), userID)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("GetVoiceState: %w", err)
}
vs := VoiceState{
UserID: r.UserID,
ChannelID: r.ChannelID,
Username: r.Username,
Muted: r.Muted != 0,
Deafened: r.Deafened != 0,
Speaking: r.Speaking != 0,
Camera: r.Camera != 0,
Screenshare: r.Screenshare != 0,
JoinedAt: r.JoinedAt,
}
return &vs, nil
}
// GetChannelVoiceStates returns all voice states for users currently in the
// given voice channel.
func (d *DB) GetChannelVoiceStates(channelID int64) ([]VoiceState, error) {
rows, err := d.q.GetChannelVoiceStates(dbCtx(), channelID)
if err != nil {
return nil, fmt.Errorf("GetChannelVoiceStates: %w", err)
}
states := make([]VoiceState, 0, len(rows))
for _, r := range rows {
states = append(states, VoiceState{
UserID: r.UserID,
ChannelID: r.ChannelID,
Username: r.Username,
Muted: r.Muted != 0,
Deafened: r.Deafened != 0,
Speaking: r.Speaking != 0,
Camera: r.Camera != 0,
Screenshare: r.Screenshare != 0,
JoinedAt: r.JoinedAt,
})
}
return states, nil
}
// GetAllVoiceStates returns voice states across all voice channels in a single
// query. Used at startup to build the ready payload without N+1 per-channel queries.
func (d *DB) GetAllVoiceStates() ([]VoiceState, error) {
rows, err := d.q.GetAllVoiceStates(dbCtx())
if err != nil {
return nil, fmt.Errorf("GetAllVoiceStates: %w", err)
}
states := make([]VoiceState, 0, len(rows))
for _, r := range rows {
states = append(states, VoiceState{
UserID: r.UserID,
ChannelID: r.ChannelID,
Username: r.Username,
Muted: r.Muted != 0,
Deafened: r.Deafened != 0,
Speaking: r.Speaking != 0,
Camera: r.Camera != 0,
Screenshare: r.Screenshare != 0,
JoinedAt: r.JoinedAt,
})
}
return states, nil
}
// UpdateVoiceMute sets the muted field for the given user's voice state.
// It is safe to call when the user is not in any channel (no-op).
func (d *DB) UpdateVoiceMute(userID int64, muted bool) error {
if err := d.q.UpdateVoiceMute(dbCtx(), dbgen.UpdateVoiceMuteParams{
Muted: b2i64(muted),
UserID: userID,
}); err != nil {
return fmt.Errorf("UpdateVoiceMute: %w", err)
}
return nil
}
// UpdateVoiceDeafen sets the deafened field for the given user's voice state.
// It is safe to call when the user is not in any channel (no-op).
func (d *DB) UpdateVoiceDeafen(userID int64, deafened bool) error {
if err := d.q.UpdateVoiceDeafen(dbCtx(), dbgen.UpdateVoiceDeafenParams{
Deafened: b2i64(deafened),
UserID: userID,
}); err != nil {
return fmt.Errorf("UpdateVoiceDeafen: %w", err)
}
return nil
}
// ClearVoiceState removes a user's voice state on disconnect.
// Equivalent to LeaveVoiceChannel but named to clarify the disconnect use case.
func (d *DB) ClearVoiceState(userID int64) error {
if err := d.q.ClearVoiceState(dbCtx(), userID); err != nil {
return fmt.Errorf("ClearVoiceState: %w", err)
}
return nil
}
// ClearAllVoiceStates removes all voice state rows. Called on server startup
// to clear stale state from a previous run.
func (d *DB) ClearAllVoiceStates() error {
if err := d.q.ClearAllVoiceStates(dbCtx()); err != nil {
return fmt.Errorf("ClearAllVoiceStates: %w", err)
}
return nil
}
// CountActiveCameras returns the number of users with camera enabled in the
// given voice channel. Uses the DB as source of truth (race-free via SQLite
// serialization) rather than querying LiveKit.
func (d *DB) CountActiveCameras(channelID int64) (int, error) {
count, err := d.q.CountActiveCameras(dbCtx(), channelID)
if err != nil {
return 0, fmt.Errorf("CountActiveCameras: %w", err)
}
return int(count), nil
}
// UpdateVoiceCamera sets the camera field for the given user's voice state.
func (d *DB) UpdateVoiceCamera(userID int64, camera bool) error {
if err := d.q.UpdateVoiceCamera(dbCtx(), dbgen.UpdateVoiceCameraParams{
Camera: b2i64(camera),
UserID: userID,
}); err != nil {
return fmt.Errorf("UpdateVoiceCamera: %w", err)
}
return nil
}
// EnableCameraIfUnderLimit atomically enables a user's camera only if the
// channel has not yet reached maxVideo active cameras. Returns true if the
// camera was enabled, false if the limit was already reached.
func (d *DB) EnableCameraIfUnderLimit(userID, channelID int64, maxVideo int) (bool, error) {
res, err := d.q.EnableCameraIfUnderLimit(dbCtx(), dbgen.EnableCameraIfUnderLimitParams{
UserID: userID,
ChannelID: channelID,
ChannelID_2: channelID,
ChannelID_3: int64(maxVideo),
})
if err != nil {
return false, fmt.Errorf("EnableCameraIfUnderLimit: %w", err)
}
rows, err := res.RowsAffected()
if err != nil {
return false, fmt.Errorf("EnableCameraIfUnderLimit RowsAffected: %w", err)
}
return rows > 0, nil
}
// UpdateVoiceScreenshare sets the screenshare field for the given user's voice state.
func (d *DB) UpdateVoiceScreenshare(userID int64, screenshare bool) error {
if err := d.q.UpdateVoiceScreenshare(dbCtx(), dbgen.UpdateVoiceScreenshareParams{
Screenshare: b2i64(screenshare),
UserID: userID,
}); err != nil {
return fmt.Errorf("UpdateVoiceScreenshare: %w", err)
}
return nil
}
// CountChannelVoiceUsers returns the number of users currently in the given
// voice channel.
func (d *DB) CountChannelVoiceUsers(channelID int64) (int, error) {
var count int
err := d.sqlDB.QueryRow(
`SELECT COUNT(*) FROM voice_states WHERE channel_id = ?`,
channelID,
).Scan(&count)
if err != nil {
return 0, fmt.Errorf("CountChannelVoiceUsers: %w", err)
}
return count, nil
}