mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
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
This commit is contained in:
+86
-142
@@ -6,14 +6,16 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/owncord/server/db/dbgen"
|
||||
)
|
||||
|
||||
// ─── Setup ───────────────────────────────────────────────────────────────────
|
||||
|
||||
// UserCount returns the total number of registered users.
|
||||
func (d *DB) UserCount() (int64, error) {
|
||||
var count int64
|
||||
if err := d.sqlDB.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&count); err != nil {
|
||||
count, err := d.q.UserCount(dbCtx())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("UserCount: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
@@ -26,21 +28,23 @@ func (d *DB) UserCount() (int64, error) {
|
||||
// a meaningful value only for file-backed databases).
|
||||
func (d *DB) GetServerStats() (*ServerStats, error) {
|
||||
stats := &ServerStats{}
|
||||
var err error
|
||||
|
||||
if err := d.sqlDB.QueryRow(`SELECT COUNT(*) FROM users`).Scan(&stats.UserCount); err != nil {
|
||||
if stats.UserCount, err = d.q.CountUsers(dbCtx()); err != nil {
|
||||
return nil, fmt.Errorf("GetServerStats users: %w", err)
|
||||
}
|
||||
if err := d.sqlDB.QueryRow(`SELECT COUNT(*) FROM messages WHERE deleted = 0`).Scan(&stats.MessageCount); err != nil {
|
||||
if stats.MessageCount, err = d.q.CountActiveMessages(dbCtx()); err != nil {
|
||||
return nil, fmt.Errorf("GetServerStats messages: %w", err)
|
||||
}
|
||||
if err := d.sqlDB.QueryRow(`SELECT COUNT(*) FROM channels`).Scan(&stats.ChannelCount); err != nil {
|
||||
if stats.ChannelCount, err = d.q.CountChannels(dbCtx()); err != nil {
|
||||
return nil, fmt.Errorf("GetServerStats channels: %w", err)
|
||||
}
|
||||
if err := d.sqlDB.QueryRow(`SELECT COUNT(*) FROM invites WHERE revoked = 0`).Scan(&stats.InviteCount); err != nil {
|
||||
if stats.InviteCount, err = d.q.CountActiveInvites(dbCtx()); err != nil {
|
||||
return nil, fmt.Errorf("GetServerStats invites: %w", err)
|
||||
}
|
||||
|
||||
// page_count * page_size gives the database size in bytes.
|
||||
// page_count * page_size gives the database size in bytes. PRAGMAs are not
|
||||
// expressible as sqlc queries, so they stay on the raw connection.
|
||||
// For :memory: databases this still works (returns the in-memory size).
|
||||
var pageCount, pageSize int64
|
||||
if err := d.sqlDB.QueryRow(`PRAGMA page_count`).Scan(&pageCount); err != nil {
|
||||
@@ -59,53 +63,40 @@ func (d *DB) GetServerStats() (*ServerStats, error) {
|
||||
// ListAllUsers returns users joined with their role name, ordered by ID.
|
||||
// limit=0 returns no rows.
|
||||
func (d *DB) ListAllUsers(limit, offset int) ([]UserWithRole, error) {
|
||||
rows, err := d.sqlDB.Query(
|
||||
`SELECT u.id, u.username, u.avatar, u.role_id,
|
||||
u.status, u.created_at, u.last_seen, u.banned, u.ban_reason, u.ban_expires,
|
||||
COALESCE(r.name, '') AS role_name
|
||||
FROM users u
|
||||
LEFT JOIN roles r ON r.id = u.role_id
|
||||
ORDER BY u.id ASC
|
||||
LIMIT ? OFFSET ?`,
|
||||
limit, offset,
|
||||
)
|
||||
rows, err := d.q.ListAllUsers(dbCtx(), dbgen.ListAllUsersParams{
|
||||
Limit: int64(limit),
|
||||
Offset: int64(offset),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ListAllUsers: %w", err)
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck
|
||||
|
||||
var result []UserWithRole
|
||||
for rows.Next() {
|
||||
var uwr UserWithRole
|
||||
var banned int
|
||||
err := rows.Scan(
|
||||
&uwr.ID, &uwr.Username, &uwr.Avatar, &uwr.RoleID,
|
||||
&uwr.Status, &uwr.CreatedAt, &uwr.LastSeen,
|
||||
&banned, &uwr.BanReason, &uwr.BanExpires,
|
||||
&uwr.RoleName,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ListAllUsers scan: %w", err)
|
||||
}
|
||||
uwr.Banned = banned != 0
|
||||
result = append(result, uwr)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, fmt.Errorf("ListAllUsers rows: %w", rows.Err())
|
||||
}
|
||||
if result == nil {
|
||||
result = []UserWithRole{}
|
||||
result := make([]UserWithRole, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
result = append(result, UserWithRole{
|
||||
User: User{
|
||||
ID: r.ID,
|
||||
Username: r.Username,
|
||||
Avatar: r.Avatar,
|
||||
RoleID: r.RoleID,
|
||||
Status: r.Status,
|
||||
CreatedAt: r.CreatedAt,
|
||||
LastSeen: r.LastSeen,
|
||||
Banned: r.Banned != 0,
|
||||
BanReason: r.BanReason,
|
||||
BanExpires: r.BanExpires,
|
||||
},
|
||||
RoleName: r.RoleName,
|
||||
})
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// UpdateUserRole changes the role_id of a user.
|
||||
func (d *DB) UpdateUserRole(userID, roleID int64) error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`UPDATE users SET role_id = ? WHERE id = ?`,
|
||||
roleID, userID,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.UpdateUserRole(dbCtx(), dbgen.UpdateUserRoleParams{
|
||||
RoleID: roleID,
|
||||
ID: userID,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("UpdateUserRole: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -113,8 +104,7 @@ func (d *DB) UpdateUserRole(userID, roleID int64) error {
|
||||
|
||||
// ForceLogoutUser deletes all sessions for the given user ID.
|
||||
func (d *DB) ForceLogoutUser(userID int64) error {
|
||||
_, err := d.sqlDB.Exec(`DELETE FROM sessions WHERE user_id = ?`, userID)
|
||||
if err != nil {
|
||||
if err := d.q.ForceLogoutUser(dbCtx(), userID); err != nil {
|
||||
return fmt.Errorf("ForceLogoutUser: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -122,33 +112,13 @@ func (d *DB) ForceLogoutUser(userID int64) error {
|
||||
|
||||
// GetUserSessions returns all active sessions for the given user ID.
|
||||
func (d *DB) GetUserSessions(userID int64) ([]Session, error) {
|
||||
rows, err := d.sqlDB.Query(
|
||||
`SELECT id, user_id, token, device, ip_address, created_at, last_used, expires_at
|
||||
FROM sessions WHERE user_id = ? ORDER BY created_at DESC`,
|
||||
userID,
|
||||
)
|
||||
rows, err := d.q.GetUserSessions(dbCtx(), userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetUserSessions: %w", err)
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck
|
||||
|
||||
var sessions []Session
|
||||
for rows.Next() {
|
||||
var s Session
|
||||
err := rows.Scan(
|
||||
&s.ID, &s.UserID, &s.TokenHash, &s.Device, &s.IP,
|
||||
&s.CreatedAt, &s.LastUsed, &s.ExpiresAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetUserSessions scan: %w", err)
|
||||
}
|
||||
sessions = append(sessions, s)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, fmt.Errorf("GetUserSessions rows: %w", rows.Err())
|
||||
}
|
||||
if sessions == nil {
|
||||
sessions = []Session{}
|
||||
sessions := make([]Session, 0, len(rows))
|
||||
for _, s := range rows {
|
||||
sessions = append(sessions, sessionFromGen(s))
|
||||
}
|
||||
return sessions, nil
|
||||
}
|
||||
@@ -156,11 +126,12 @@ func (d *DB) GetUserSessions(userID int64) ([]Session, error) {
|
||||
// ─── Channel Management (admin) ───────────────────────────────────────────────
|
||||
|
||||
// AdminCreateChannel creates a channel with full field control including position.
|
||||
// No sqlc query covers this exact INSERT shape, so it stays on raw SQL.
|
||||
func (d *DB) AdminCreateChannel(name, chanType, category, topic string, position int) (int64, error) {
|
||||
res, err := d.sqlDB.Exec(
|
||||
`INSERT INTO channels (name, type, category, topic, position)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
name, chanType, nullableString(category), nullableString(topic), position,
|
||||
name, chanType, strToNullPtr(category), strToNullPtr(topic), position,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("AdminCreateChannel: %w", err)
|
||||
@@ -170,17 +141,14 @@ func (d *DB) AdminCreateChannel(name, chanType, category, topic string, position
|
||||
|
||||
// AdminUpdateChannel updates all mutable channel fields.
|
||||
func (d *DB) AdminUpdateChannel(id int64, name, topic string, slowMode, position int, archived bool) error {
|
||||
archivedInt := 0
|
||||
if archived {
|
||||
archivedInt = 1
|
||||
}
|
||||
_, err := d.sqlDB.Exec(
|
||||
`UPDATE channels
|
||||
SET name = ?, topic = ?, slow_mode = ?, position = ?, archived = ?
|
||||
WHERE id = ?`,
|
||||
name, nullableString(topic), slowMode, position, archivedInt, id,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.AdminUpdateChannel(dbCtx(), dbgen.AdminUpdateChannelParams{
|
||||
Name: name,
|
||||
Topic: strToNullPtr(topic),
|
||||
SlowMode: int64(slowMode),
|
||||
Position: int64(position),
|
||||
Archived: b2i64(archived),
|
||||
ID: id,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("AdminUpdateChannel: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -188,8 +156,7 @@ func (d *DB) AdminUpdateChannel(id int64, name, topic string, slowMode, position
|
||||
|
||||
// AdminDeleteChannel removes a channel by ID (cascades to messages, etc.).
|
||||
func (d *DB) AdminDeleteChannel(id int64) error {
|
||||
_, err := d.sqlDB.Exec(`DELETE FROM channels WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
if err := d.q.DeleteChannel(dbCtx(), id); err != nil {
|
||||
return fmt.Errorf("AdminDeleteChannel: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -199,12 +166,13 @@ func (d *DB) AdminDeleteChannel(id int64) error {
|
||||
|
||||
// LogAudit inserts an audit log entry.
|
||||
func (d *DB) LogAudit(actorID int64, action, targetType string, targetID int64, detail string) error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`INSERT INTO audit_log (actor_id, action, target_type, target_id, detail)
|
||||
VALUES (?, ?, ?, ?, ?)`,
|
||||
actorID, action, targetType, targetID, detail,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.LogAudit(dbCtx(), dbgen.LogAuditParams{
|
||||
ActorID: actorID,
|
||||
Action: action,
|
||||
TargetType: targetType,
|
||||
TargetID: targetID,
|
||||
Detail: detail,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("LogAudit: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -212,36 +180,25 @@ func (d *DB) LogAudit(actorID int64, action, targetType string, targetID int64,
|
||||
|
||||
// GetAuditLog returns audit log entries ordered newest-first with pagination.
|
||||
func (d *DB) GetAuditLog(limit, offset int) ([]AuditEntry, error) {
|
||||
rows, err := d.sqlDB.Query(
|
||||
`SELECT a.id, a.actor_id, COALESCE(u.username, ''), a.action,
|
||||
a.target_type, a.target_id, a.detail, a.created_at
|
||||
FROM audit_log a
|
||||
LEFT JOIN users u ON u.id = a.actor_id
|
||||
ORDER BY a.id DESC
|
||||
LIMIT ? OFFSET ?`,
|
||||
limit, offset,
|
||||
)
|
||||
rows, err := d.q.GetAuditLog(dbCtx(), dbgen.GetAuditLogParams{
|
||||
Limit: int64(limit),
|
||||
Offset: int64(offset),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetAuditLog: %w", err)
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck
|
||||
|
||||
var entries []AuditEntry
|
||||
for rows.Next() {
|
||||
var e AuditEntry
|
||||
if err := rows.Scan(
|
||||
&e.ID, &e.ActorID, &e.ActorName, &e.Action,
|
||||
&e.TargetType, &e.TargetID, &e.Detail, &e.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, fmt.Errorf("GetAuditLog scan: %w", err)
|
||||
}
|
||||
entries = append(entries, e)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, fmt.Errorf("GetAuditLog rows: %w", rows.Err())
|
||||
}
|
||||
if entries == nil {
|
||||
entries = []AuditEntry{}
|
||||
entries := make([]AuditEntry, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
entries = append(entries, AuditEntry{
|
||||
ID: r.ID,
|
||||
ActorID: r.ActorID,
|
||||
ActorName: r.ActorName,
|
||||
Action: r.Action,
|
||||
TargetType: r.TargetType,
|
||||
TargetID: r.TargetID,
|
||||
Detail: r.Detail,
|
||||
CreatedAt: r.CreatedAt,
|
||||
})
|
||||
}
|
||||
return entries, nil
|
||||
}
|
||||
@@ -251,8 +208,7 @@ func (d *DB) GetAuditLog(limit, offset int) ([]AuditEntry, error) {
|
||||
// GetSetting returns the value for the given settings key.
|
||||
// Returns an error (wrapping sql.ErrNoRows) when the key does not exist.
|
||||
func (d *DB) GetSetting(key string) (string, error) {
|
||||
var value string
|
||||
err := d.sqlDB.QueryRow(`SELECT value FROM settings WHERE key = ?`, key).Scan(&value)
|
||||
value, err := d.q.GetSetting(dbCtx(), key)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return "", fmt.Errorf("GetSetting: key %q: %w", key, ErrNotFound)
|
||||
}
|
||||
@@ -264,12 +220,10 @@ func (d *DB) GetSetting(key string) (string, error) {
|
||||
|
||||
// SetSetting upserts a setting value for the given key.
|
||||
func (d *DB) SetSetting(key, value string) error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`INSERT INTO settings (key, value) VALUES (?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value = excluded.value`,
|
||||
key, value,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.SetSetting(dbCtx(), dbgen.SetSettingParams{
|
||||
Key: key,
|
||||
Value: value,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("SetSetting: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -277,22 +231,13 @@ func (d *DB) SetSetting(key, value string) error {
|
||||
|
||||
// GetAllSettings returns all settings as a key→value map.
|
||||
func (d *DB) GetAllSettings() (map[string]string, error) {
|
||||
rows, err := d.sqlDB.Query(`SELECT key, value FROM settings`)
|
||||
rows, err := d.q.GetAllSettings(dbCtx())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetAllSettings: %w", err)
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck
|
||||
|
||||
result := make(map[string]string)
|
||||
for rows.Next() {
|
||||
var k, v string
|
||||
if err := rows.Scan(&k, &v); err != nil {
|
||||
return nil, fmt.Errorf("GetAllSettings scan: %w", err)
|
||||
}
|
||||
result[k] = v
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, fmt.Errorf("GetAllSettings rows: %w", rows.Err())
|
||||
result := make(map[string]string, len(rows))
|
||||
for _, s := range rows {
|
||||
result[s.Key] = s.Value
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -300,12 +245,11 @@ func (d *DB) GetAllSettings() (map[string]string, error) {
|
||||
// CountUsersWithoutTOTP returns the number of non-banned users that do not
|
||||
// currently have a confirmed TOTP secret.
|
||||
func (d *DB) CountUsersWithoutTOTP() (int, error) {
|
||||
var count int
|
||||
err := d.sqlDB.QueryRow(`SELECT COUNT(*) FROM users WHERE banned = 0 AND totp_secret IS NULL`).Scan(&count)
|
||||
count, err := d.q.CountUsersWithoutTOTP(dbCtx())
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("CountUsersWithoutTOTP: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
return int(count), nil
|
||||
}
|
||||
|
||||
// ─── Backup ───────────────────────────────────────────────────────────────────
|
||||
|
||||
+87
-125
@@ -5,76 +5,82 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/owncord/server/db/dbgen"
|
||||
)
|
||||
|
||||
// channelFields carries the 13 columns shared by GetChannelRow and
|
||||
// ListChannelsRow; both generated row types are structurally identical, so a
|
||||
// single mapper narrows either to the domain Channel model.
|
||||
type channelFields struct {
|
||||
ID int64
|
||||
Name string
|
||||
Type string
|
||||
Category string
|
||||
Topic string
|
||||
Position int64
|
||||
SlowMode int64
|
||||
Archived int64
|
||||
CreatedAt string
|
||||
VoiceMaxUsers int64
|
||||
VoiceQuality *string
|
||||
MixingThreshold *int64
|
||||
VoiceMaxVideo int64
|
||||
}
|
||||
|
||||
func channelFromFields(f channelFields) Channel {
|
||||
return Channel{
|
||||
ID: f.ID,
|
||||
Name: f.Name,
|
||||
Type: f.Type,
|
||||
Category: f.Category,
|
||||
Topic: f.Topic,
|
||||
Position: int(f.Position),
|
||||
SlowMode: int(f.SlowMode),
|
||||
Archived: f.Archived != 0,
|
||||
CreatedAt: f.CreatedAt,
|
||||
VoiceMaxUsers: int(f.VoiceMaxUsers),
|
||||
VoiceQuality: f.VoiceQuality,
|
||||
MixingThreshold: ptrI64toI(f.MixingThreshold),
|
||||
VoiceMaxVideo: int(f.VoiceMaxVideo),
|
||||
}
|
||||
}
|
||||
|
||||
// ListChannels returns all channels ordered by position.
|
||||
func (d *DB) ListChannels() ([]Channel, error) {
|
||||
rows, err := d.sqlDB.Query(
|
||||
`SELECT id, name, type, COALESCE(category,''), COALESCE(topic,''),
|
||||
position, slow_mode, archived, created_at,
|
||||
COALESCE(voice_max_users, 0),
|
||||
voice_quality,
|
||||
mixing_threshold,
|
||||
COALESCE(voice_max_video, 0)
|
||||
FROM channels ORDER BY position ASC, id ASC`,
|
||||
)
|
||||
rows, err := d.q.ListChannels(dbCtx())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ListChannels: %w", err)
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck
|
||||
|
||||
var channels []Channel
|
||||
for rows.Next() {
|
||||
ch, scanErr := scanChannel(rows)
|
||||
if scanErr != nil {
|
||||
return nil, fmt.Errorf("ListChannels scan: %w", scanErr)
|
||||
}
|
||||
channels = append(channels, ch)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, fmt.Errorf("ListChannels rows: %w", rows.Err())
|
||||
}
|
||||
if channels == nil {
|
||||
channels = []Channel{}
|
||||
channels := make([]Channel, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
channels = append(channels, channelFromFields(channelFields(r)))
|
||||
}
|
||||
return channels, nil
|
||||
}
|
||||
|
||||
// GetChannel returns the channel with the given id, or nil if not found.
|
||||
func (d *DB) GetChannel(id int64) (*Channel, error) {
|
||||
row := d.sqlDB.QueryRow(
|
||||
`SELECT id, name, type, COALESCE(category,''), COALESCE(topic,''),
|
||||
position, slow_mode, archived, created_at,
|
||||
COALESCE(voice_max_users, 0),
|
||||
voice_quality,
|
||||
mixing_threshold,
|
||||
COALESCE(voice_max_video, 0)
|
||||
FROM channels WHERE id = ?`,
|
||||
id,
|
||||
)
|
||||
ch := &Channel{}
|
||||
var archived int
|
||||
err := row.Scan(
|
||||
&ch.ID, &ch.Name, &ch.Type, &ch.Category, &ch.Topic,
|
||||
&ch.Position, &ch.SlowMode, &archived, &ch.CreatedAt,
|
||||
&ch.VoiceMaxUsers, &ch.VoiceQuality, &ch.MixingThreshold, &ch.VoiceMaxVideo,
|
||||
)
|
||||
r, err := d.q.GetChannel(dbCtx(), id)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetChannel: %w", err)
|
||||
}
|
||||
ch.Archived = archived != 0
|
||||
return ch, nil
|
||||
ch := channelFromFields(channelFields(r))
|
||||
return &ch, nil
|
||||
}
|
||||
|
||||
// CreateChannel inserts a new channel and returns the assigned ID.
|
||||
func (d *DB) CreateChannel(name, chanType, category, topic string, position int) (int64, error) {
|
||||
res, err := d.sqlDB.Exec(
|
||||
`INSERT INTO channels (name, type, category, topic, position) VALUES (?, ?, ?, ?, ?)`,
|
||||
name, chanType, nullableString(category), nullableString(topic), position,
|
||||
)
|
||||
res, err := d.q.CreateChannel(dbCtx(), dbgen.CreateChannelParams{
|
||||
Name: name,
|
||||
Type: chanType,
|
||||
Category: strToNullPtr(category),
|
||||
Topic: strToNullPtr(topic),
|
||||
Position: int64(position),
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("CreateChannel: %w", err)
|
||||
}
|
||||
@@ -83,11 +89,12 @@ func (d *DB) CreateChannel(name, chanType, category, topic string, position int)
|
||||
|
||||
// UpdateChannel modifies name, topic, and slow_mode for the given channel.
|
||||
func (d *DB) UpdateChannel(id int64, name, topic string, slowMode int) error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`UPDATE channels SET name = ?, topic = ?, slow_mode = ? WHERE id = ?`,
|
||||
name, nullableString(topic), slowMode, id,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.UpdateChannel(dbCtx(), dbgen.UpdateChannelParams{
|
||||
Name: name,
|
||||
Topic: strToNullPtr(topic),
|
||||
SlowMode: int64(slowMode),
|
||||
ID: id,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("UpdateChannel: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -95,11 +102,10 @@ func (d *DB) UpdateChannel(id int64, name, topic string, slowMode int) error {
|
||||
|
||||
// SetChannelSlowMode updates only the slow_mode field for the given channel.
|
||||
func (d *DB) SetChannelSlowMode(id int64, slowMode int) error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`UPDATE channels SET slow_mode = ? WHERE id = ?`,
|
||||
slowMode, id,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.SetChannelSlowMode(dbCtx(), dbgen.SetChannelSlowModeParams{
|
||||
SlowMode: int64(slowMode),
|
||||
ID: id,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("SetChannelSlowMode: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -107,8 +113,10 @@ func (d *DB) SetChannelSlowMode(id int64, slowMode int) error {
|
||||
|
||||
// SetChannelVoiceMaxUsers updates the voice_max_users field for the given channel.
|
||||
func (d *DB) SetChannelVoiceMaxUsers(id int64, maxUsers int) error {
|
||||
_, err := d.sqlDB.Exec(`UPDATE channels SET voice_max_users = ? WHERE id = ?`, maxUsers, id)
|
||||
if err != nil {
|
||||
if err := d.q.SetChannelVoiceMaxUsers(dbCtx(), dbgen.SetChannelVoiceMaxUsersParams{
|
||||
VoiceMaxUsers: int64(maxUsers),
|
||||
ID: id,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("SetChannelVoiceMaxUsers: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -116,8 +124,7 @@ func (d *DB) SetChannelVoiceMaxUsers(id int64, maxUsers int) error {
|
||||
|
||||
// DeleteChannel removes the channel row (cascades to messages, overrides, etc.).
|
||||
func (d *DB) DeleteChannel(id int64) error {
|
||||
_, err := d.sqlDB.Exec(`DELETE FROM channels WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
if err := d.q.DeleteChannel(dbCtx(), id); err != nil {
|
||||
return fmt.Errorf("DeleteChannel: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -126,18 +133,17 @@ func (d *DB) DeleteChannel(id int64) error {
|
||||
// GetChannelPermissions returns the allow/deny override bits for a role on a
|
||||
// channel. Returns (0, 0, nil) when no override exists.
|
||||
func (d *DB) GetChannelPermissions(channelID, roleID int64) (allow, deny int64, err error) {
|
||||
row := d.sqlDB.QueryRow(
|
||||
`SELECT allow, deny FROM channel_overrides WHERE channel_id = ? AND role_id = ?`,
|
||||
channelID, roleID,
|
||||
)
|
||||
scanErr := row.Scan(&allow, &deny)
|
||||
r, scanErr := d.q.GetChannelPermission(dbCtx(), dbgen.GetChannelPermissionParams{
|
||||
ChannelID: channelID,
|
||||
RoleID: roleID,
|
||||
})
|
||||
if errors.Is(scanErr, sql.ErrNoRows) {
|
||||
return 0, 0, nil
|
||||
}
|
||||
if scanErr != nil {
|
||||
return 0, 0, fmt.Errorf("GetChannelPermissions: %w", scanErr)
|
||||
}
|
||||
return allow, deny, nil
|
||||
return r.Allow, r.Deny, nil
|
||||
}
|
||||
|
||||
// ChannelOverride holds the allow/deny permission bits for a single channel.
|
||||
@@ -150,26 +156,13 @@ type ChannelOverride struct {
|
||||
// a role in a single query, keyed by channel ID. Eliminates N+1 queries when
|
||||
// filtering channels by permission.
|
||||
func (d *DB) GetAllChannelPermissionsForRole(roleID int64) (map[int64]ChannelOverride, error) {
|
||||
rows, err := d.sqlDB.Query(
|
||||
`SELECT channel_id, allow, deny FROM channel_overrides WHERE role_id = ?`,
|
||||
roleID,
|
||||
)
|
||||
rows, err := d.q.GetRoleChannelPermissions(dbCtx(), roleID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetAllChannelPermissionsForRole: %w", err)
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck
|
||||
|
||||
result := make(map[int64]ChannelOverride)
|
||||
for rows.Next() {
|
||||
var chID int64
|
||||
var o ChannelOverride
|
||||
if scanErr := rows.Scan(&chID, &o.Allow, &o.Deny); scanErr != nil {
|
||||
return nil, fmt.Errorf("GetAllChannelPermissionsForRole scan: %w", scanErr)
|
||||
}
|
||||
result[chID] = o
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, fmt.Errorf("GetAllChannelPermissionsForRole rows: %w", rows.Err())
|
||||
result := make(map[int64]ChannelOverride, len(rows))
|
||||
for _, r := range rows {
|
||||
result[r.ChannelID] = ChannelOverride{Allow: r.Allow, Deny: r.Deny}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
@@ -177,14 +170,12 @@ func (d *DB) GetAllChannelPermissionsForRole(roleID int64) (map[int64]ChannelOve
|
||||
// UpsertChannelOverride inserts or updates the allow/deny permission override
|
||||
// for a role on a channel.
|
||||
func (d *DB) UpsertChannelOverride(channelID, roleID, allow, deny int64) error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`INSERT INTO channel_overrides (channel_id, role_id, allow, deny)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(channel_id, role_id)
|
||||
DO UPDATE SET allow = excluded.allow, deny = excluded.deny`,
|
||||
channelID, roleID, allow, deny,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.UpsertChannelPermission(dbCtx(), dbgen.UpsertChannelPermissionParams{
|
||||
ChannelID: channelID,
|
||||
RoleID: roleID,
|
||||
Allow: allow,
|
||||
Deny: deny,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("UpsertChannelOverride: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -193,11 +184,10 @@ func (d *DB) UpsertChannelOverride(channelID, roleID, allow, deny int64) error {
|
||||
// DeleteChannelOverride removes the permission override for a role on a
|
||||
// channel. Deleting a non-existent override is a no-op.
|
||||
func (d *DB) DeleteChannelOverride(channelID, roleID int64) error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`DELETE FROM channel_overrides WHERE channel_id = ? AND role_id = ?`,
|
||||
channelID, roleID,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.DeleteChannelPermission(dbCtx(), dbgen.DeleteChannelPermissionParams{
|
||||
ChannelID: channelID,
|
||||
RoleID: roleID,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("DeleteChannelOverride: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -251,34 +241,6 @@ func (d *DB) ListChannelRoleOverrides(channelID int64) ([]ChannelRoleOverride, e
|
||||
|
||||
// ─── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// scanChannel scans a single channel row from *sql.Rows.
|
||||
// The query must select the 13 columns: id, name, type, category, topic,
|
||||
// position, slow_mode, archived, created_at, voice_max_users,
|
||||
// voice_quality, mixing_threshold, voice_max_video.
|
||||
func scanChannel(rows *sql.Rows) (Channel, error) {
|
||||
var ch Channel
|
||||
var archived int
|
||||
err := rows.Scan(
|
||||
&ch.ID, &ch.Name, &ch.Type, &ch.Category, &ch.Topic,
|
||||
&ch.Position, &ch.SlowMode, &archived, &ch.CreatedAt,
|
||||
&ch.VoiceMaxUsers, &ch.VoiceQuality, &ch.MixingThreshold, &ch.VoiceMaxVideo,
|
||||
)
|
||||
if err != nil {
|
||||
return Channel{}, err
|
||||
}
|
||||
ch.Archived = archived != 0
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
// nullableString returns nil when s is empty, otherwise a pointer to s.
|
||||
// Used so empty strings are stored as NULL in optional TEXT columns.
|
||||
func nullableString(s string) any {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// GetChannelTypes returns a map of channel ID → type string for the given IDs
|
||||
// in a single query, avoiding N+1 lookups.
|
||||
func (d *DB) GetChannelTypes(ids []int64) (map[int64]string, error) {
|
||||
|
||||
+15
-32
@@ -5,6 +5,8 @@ import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/owncord/server/db/dbgen"
|
||||
)
|
||||
|
||||
// ─── DM Models ──────────────────────────────────────────────────────────────
|
||||
@@ -202,11 +204,10 @@ func (d *DB) GetUserDMChannels(userID int64) ([]DMChannelInfo, error) {
|
||||
|
||||
// OpenDM adds a DM channel to a user's open list (idempotent).
|
||||
func (d *DB) OpenDM(userID, channelID int64) error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`INSERT OR IGNORE INTO dm_open_state (user_id, channel_id) VALUES (?, ?)`,
|
||||
userID, channelID,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.OpenDM(dbCtx(), dbgen.OpenDMParams{
|
||||
UserID: userID,
|
||||
ChannelID: channelID,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("OpenDM: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -214,11 +215,10 @@ func (d *DB) OpenDM(userID, channelID int64) error {
|
||||
|
||||
// CloseDM removes a DM channel from a user's open list.
|
||||
func (d *DB) CloseDM(userID, channelID int64) error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`DELETE FROM dm_open_state WHERE user_id = ? AND channel_id = ?`,
|
||||
userID, channelID,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.CloseDM(dbCtx(), dbgen.CloseDMParams{
|
||||
UserID: userID,
|
||||
ChannelID: channelID,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("CloseDM: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -228,11 +228,10 @@ func (d *DB) CloseDM(userID, channelID int64) error {
|
||||
|
||||
// IsDMParticipant checks if a user is a participant in a DM channel.
|
||||
func (d *DB) IsDMParticipant(userID, channelID int64) (bool, error) {
|
||||
var id int64
|
||||
err := d.sqlDB.QueryRow(
|
||||
`SELECT user_id FROM dm_participants WHERE user_id = ? AND channel_id = ?`,
|
||||
userID, channelID,
|
||||
).Scan(&id)
|
||||
_, err := d.q.IsDMParticipant(dbCtx(), dbgen.IsDMParticipantParams{
|
||||
UserID: userID,
|
||||
ChannelID: channelID,
|
||||
})
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return false, nil
|
||||
}
|
||||
@@ -244,26 +243,10 @@ func (d *DB) IsDMParticipant(userID, channelID int64) (bool, error) {
|
||||
|
||||
// GetDMParticipantIDs returns all participant user IDs for a DM channel.
|
||||
func (d *DB) GetDMParticipantIDs(channelID int64) ([]int64, error) {
|
||||
rows, err := d.sqlDB.Query(
|
||||
`SELECT user_id FROM dm_participants WHERE channel_id = ?`,
|
||||
channelID,
|
||||
)
|
||||
ids, err := d.q.GetDMParticipantIDs(dbCtx(), channelID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetDMParticipantIDs: %w", err)
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck
|
||||
|
||||
var ids []int64
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
if scanErr := rows.Scan(&id); scanErr != nil {
|
||||
return nil, fmt.Errorf("GetDMParticipantIDs scan: %w", scanErr)
|
||||
}
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, fmt.Errorf("GetDMParticipantIDs rows: %w", rows.Err())
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,24 @@ func ptrItoI64(p *int) *int64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
// b2i64 converts a bool to sqlc's int64 representation of a SQLite boolean.
|
||||
func b2i64(b bool) int64 {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// strToNullPtr returns nil for an empty string, else a pointer to it — so
|
||||
// empty strings are written as NULL in optional TEXT columns (matches the
|
||||
// former nullableString helper).
|
||||
func strToNullPtr(s string) *string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
// userFromGen maps a generated user row to the domain User model.
|
||||
func userFromGen(u dbgen.User) *User {
|
||||
return &User{
|
||||
|
||||
+93
-186
@@ -6,6 +6,8 @@ import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/owncord/server/db/dbgen"
|
||||
)
|
||||
|
||||
// ErrChannelFull is returned when a voice channel is at capacity.
|
||||
@@ -26,21 +28,11 @@ func newVoiceJoinToken() string {
|
||||
// target one specific voice session even if the user later rejoins the same
|
||||
// channel.
|
||||
func (d *DB) JoinVoiceChannel(userID, channelID int64) error {
|
||||
joinToken := newVoiceJoinToken()
|
||||
_, err := d.sqlDB.Exec(
|
||||
`INSERT INTO voice_states (user_id, channel_id, muted, deafened, speaking, camera, screenshare, joined_at)
|
||||
VALUES (?, ?, 0, 0, 0, 0, 0, ?)
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
channel_id = excluded.channel_id,
|
||||
muted = 0,
|
||||
deafened = 0,
|
||||
speaking = 0,
|
||||
camera = 0,
|
||||
screenshare = 0,
|
||||
joined_at = excluded.joined_at`,
|
||||
userID, channelID, joinToken,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.JoinVoiceChannel(dbCtx(), dbgen.JoinVoiceChannelParams{
|
||||
UserID: userID,
|
||||
ChannelID: channelID,
|
||||
JoinedAt: newVoiceJoinToken(),
|
||||
}); err != nil {
|
||||
return fmt.Errorf("JoinVoiceChannel: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -51,21 +43,13 @@ func (d *DB) JoinVoiceChannel(userID, channelID int64) error {
|
||||
// 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 {
|
||||
joinToken := newVoiceJoinToken()
|
||||
res, err := d.sqlDB.Exec(
|
||||
`INSERT INTO voice_states (user_id, channel_id, muted, deafened, speaking, camera, screenshare, joined_at)
|
||||
SELECT ?, ?, 0, 0, 0, 0, 0, ?
|
||||
WHERE (SELECT COUNT(*) FROM voice_states WHERE channel_id = ?) < ?
|
||||
ON CONFLICT(user_id) DO UPDATE SET
|
||||
channel_id = excluded.channel_id,
|
||||
muted = 0,
|
||||
deafened = 0,
|
||||
speaking = 0,
|
||||
camera = 0,
|
||||
screenshare = 0,
|
||||
joined_at = excluded.joined_at`,
|
||||
userID, channelID, joinToken, channelID, maxUsers,
|
||||
)
|
||||
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)
|
||||
}
|
||||
@@ -79,8 +63,7 @@ func (d *DB) JoinVoiceChannelIfCapacity(userID, channelID int64, maxUsers int) e
|
||||
// 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 {
|
||||
_, err := d.sqlDB.Exec(`DELETE FROM voice_states WHERE user_id = ?`, userID)
|
||||
if err != nil {
|
||||
if err := d.q.LeaveVoiceChannel(dbCtx(), userID); err != nil {
|
||||
return fmt.Errorf("LeaveVoiceChannel: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -90,10 +73,11 @@ func (d *DB) LeaveVoiceChannel(userID int64) error {
|
||||
// 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.sqlDB.Exec(
|
||||
`DELETE FROM voice_states WHERE user_id = ? AND channel_id = ? AND joined_at = ?`,
|
||||
userID, expectedChannelID, expectedJoinedAt,
|
||||
)
|
||||
result, err := d.q.LeaveVoiceChannelIfMatch(dbCtx(), dbgen.LeaveVoiceChannelIfMatchParams{
|
||||
UserID: userID,
|
||||
ChannelID: expectedChannelID,
|
||||
JoinedAt: expectedJoinedAt,
|
||||
})
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("LeaveVoiceChannelIfMatch: %w", err)
|
||||
}
|
||||
@@ -104,49 +88,47 @@ func (d *DB) LeaveVoiceChannelIfMatch(userID, expectedChannelID int64, expectedJ
|
||||
// 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) {
|
||||
row := d.sqlDB.QueryRow(
|
||||
`SELECT vs.user_id, vs.channel_id, u.username,
|
||||
vs.muted, vs.deafened, vs.speaking,
|
||||
vs.camera, vs.screenshare, vs.joined_at
|
||||
FROM voice_states vs
|
||||
JOIN users u ON u.id = vs.user_id
|
||||
WHERE vs.user_id = ?`,
|
||||
userID,
|
||||
)
|
||||
return scanVoiceState(row)
|
||||
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.sqlDB.Query(
|
||||
`SELECT vs.user_id, vs.channel_id, u.username,
|
||||
vs.muted, vs.deafened, vs.speaking,
|
||||
vs.camera, vs.screenshare, vs.joined_at
|
||||
FROM voice_states vs
|
||||
JOIN users u ON u.id = vs.user_id
|
||||
WHERE vs.channel_id = ?
|
||||
ORDER BY vs.joined_at ASC`,
|
||||
channelID,
|
||||
)
|
||||
rows, err := d.q.GetChannelVoiceStates(dbCtx(), channelID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetChannelVoiceStates: %w", err)
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck
|
||||
|
||||
var states []VoiceState
|
||||
for rows.Next() {
|
||||
vs, scanErr := scanVoiceStateRow(rows)
|
||||
if scanErr != nil {
|
||||
return nil, fmt.Errorf("GetChannelVoiceStates scan: %w", scanErr)
|
||||
}
|
||||
states = append(states, vs)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, fmt.Errorf("GetChannelVoiceStates rows: %w", rows.Err())
|
||||
}
|
||||
if states == nil {
|
||||
states = []VoiceState{}
|
||||
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
|
||||
}
|
||||
@@ -154,32 +136,23 @@ func (d *DB) GetChannelVoiceStates(channelID int64) ([]VoiceState, error) {
|
||||
// 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.sqlDB.Query(
|
||||
`SELECT vs.user_id, vs.channel_id, u.username,
|
||||
vs.muted, vs.deafened, vs.speaking,
|
||||
vs.camera, vs.screenshare, vs.joined_at
|
||||
FROM voice_states vs
|
||||
JOIN users u ON u.id = vs.user_id
|
||||
ORDER BY vs.channel_id, vs.joined_at ASC`,
|
||||
)
|
||||
rows, err := d.q.GetAllVoiceStates(dbCtx())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetAllVoiceStates: %w", err)
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck
|
||||
|
||||
var states []VoiceState
|
||||
for rows.Next() {
|
||||
vs, scanErr := scanVoiceStateRow(rows)
|
||||
if scanErr != nil {
|
||||
return nil, fmt.Errorf("GetAllVoiceStates scan: %w", scanErr)
|
||||
}
|
||||
states = append(states, vs)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return nil, fmt.Errorf("GetAllVoiceStates rows: %w", rows.Err())
|
||||
}
|
||||
if states == nil {
|
||||
states = []VoiceState{}
|
||||
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
|
||||
}
|
||||
@@ -187,12 +160,10 @@ func (d *DB) GetAllVoiceStates() ([]VoiceState, error) {
|
||||
// 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 {
|
||||
muteInt := boolToInt(muted)
|
||||
_, err := d.sqlDB.Exec(
|
||||
`UPDATE voice_states SET muted = ? WHERE user_id = ?`,
|
||||
muteInt, userID,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.UpdateVoiceMute(dbCtx(), dbgen.UpdateVoiceMuteParams{
|
||||
Muted: b2i64(muted),
|
||||
UserID: userID,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("UpdateVoiceMute: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -201,12 +172,10 @@ func (d *DB) UpdateVoiceMute(userID int64, muted bool) error {
|
||||
// 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 {
|
||||
deafenInt := boolToInt(deafened)
|
||||
_, err := d.sqlDB.Exec(
|
||||
`UPDATE voice_states SET deafened = ? WHERE user_id = ?`,
|
||||
deafenInt, userID,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.UpdateVoiceDeafen(dbCtx(), dbgen.UpdateVoiceDeafenParams{
|
||||
Deafened: b2i64(deafened),
|
||||
UserID: userID,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("UpdateVoiceDeafen: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -215,8 +184,7 @@ func (d *DB) UpdateVoiceDeafen(userID int64, deafened bool) error {
|
||||
// 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 {
|
||||
_, err := d.sqlDB.Exec(`DELETE FROM voice_states WHERE user_id = ?`, userID)
|
||||
if err != nil {
|
||||
if err := d.q.ClearVoiceState(dbCtx(), userID); err != nil {
|
||||
return fmt.Errorf("ClearVoiceState: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -225,8 +193,7 @@ func (d *DB) ClearVoiceState(userID int64) error {
|
||||
// ClearAllVoiceStates removes all voice state rows. Called on server startup
|
||||
// to clear stale state from a previous run.
|
||||
func (d *DB) ClearAllVoiceStates() error {
|
||||
_, err := d.sqlDB.Exec(`DELETE FROM voice_states`)
|
||||
if err != nil {
|
||||
if err := d.q.ClearAllVoiceStates(dbCtx()); err != nil {
|
||||
return fmt.Errorf("ClearAllVoiceStates: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -236,24 +203,19 @@ func (d *DB) ClearAllVoiceStates() error {
|
||||
// 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) {
|
||||
var count int
|
||||
err := d.sqlDB.QueryRow(
|
||||
`SELECT COUNT(*) FROM voice_states WHERE channel_id = ? AND camera = 1`,
|
||||
channelID,
|
||||
).Scan(&count)
|
||||
count, err := d.q.CountActiveCameras(dbCtx(), channelID)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("CountActiveCameras: %w", err)
|
||||
}
|
||||
return count, nil
|
||||
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 {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`UPDATE voice_states SET camera = ? WHERE user_id = ?`,
|
||||
boolToInt(camera), userID,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.UpdateVoiceCamera(dbCtx(), dbgen.UpdateVoiceCameraParams{
|
||||
Camera: b2i64(camera),
|
||||
UserID: userID,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("UpdateVoiceCamera: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -263,12 +225,12 @@ func (d *DB) UpdateVoiceCamera(userID int64, camera bool) error {
|
||||
// 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.sqlDB.Exec(
|
||||
`UPDATE voice_states SET camera = 1
|
||||
WHERE user_id = ? AND channel_id = ?
|
||||
AND (SELECT COUNT(*) FROM voice_states WHERE channel_id = ? AND camera = 1) < ?`,
|
||||
userID, channelID, channelID, maxVideo,
|
||||
)
|
||||
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)
|
||||
}
|
||||
@@ -281,11 +243,10 @@ func (d *DB) EnableCameraIfUnderLimit(userID, channelID int64, maxVideo int) (bo
|
||||
|
||||
// UpdateVoiceScreenshare sets the screenshare field for the given user's voice state.
|
||||
func (d *DB) UpdateVoiceScreenshare(userID int64, screenshare bool) error {
|
||||
_, err := d.sqlDB.Exec(
|
||||
`UPDATE voice_states SET screenshare = ? WHERE user_id = ?`,
|
||||
boolToInt(screenshare), userID,
|
||||
)
|
||||
if err != nil {
|
||||
if err := d.q.UpdateVoiceScreenshare(dbCtx(), dbgen.UpdateVoiceScreenshareParams{
|
||||
Screenshare: b2i64(screenshare),
|
||||
UserID: userID,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("UpdateVoiceScreenshare: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -304,57 +265,3 @@ func (d *DB) CountChannelVoiceUsers(channelID int64) (int, error) {
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// ─── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// scanVoiceState scans a single *sql.Row into a VoiceState.
|
||||
// Returns nil (not an error) when the row is not found.
|
||||
func scanVoiceState(row *sql.Row) (*VoiceState, error) {
|
||||
vs := &VoiceState{}
|
||||
var muted, deafened, speaking, camera, screenshare int
|
||||
err := row.Scan(
|
||||
&vs.UserID, &vs.ChannelID, &vs.Username,
|
||||
&muted, &deafened, &speaking,
|
||||
&camera, &screenshare, &vs.JoinedAt,
|
||||
)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("scanVoiceState: %w", err)
|
||||
}
|
||||
vs.Muted = muted != 0
|
||||
vs.Deafened = deafened != 0
|
||||
vs.Speaking = speaking != 0
|
||||
vs.Camera = camera != 0
|
||||
vs.Screenshare = screenshare != 0
|
||||
return vs, nil
|
||||
}
|
||||
|
||||
// scanVoiceStateRow scans a single row from *sql.Rows into a VoiceState.
|
||||
func scanVoiceStateRow(rows *sql.Rows) (VoiceState, error) {
|
||||
vs := VoiceState{}
|
||||
var muted, deafened, speaking, camera, screenshare int
|
||||
err := rows.Scan(
|
||||
&vs.UserID, &vs.ChannelID, &vs.Username,
|
||||
&muted, &deafened, &speaking,
|
||||
&camera, &screenshare, &vs.JoinedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return vs, fmt.Errorf("scanVoiceStateRow: %w", err)
|
||||
}
|
||||
vs.Muted = muted != 0
|
||||
vs.Deafened = deafened != 0
|
||||
vs.Speaking = speaking != 0
|
||||
vs.Camera = camera != 0
|
||||
vs.Screenshare = screenshare != 0
|
||||
return vs, nil
|
||||
}
|
||||
|
||||
// boolToInt converts a bool to 0/1 for SQLite storage.
|
||||
func boolToInt(b bool) int {
|
||||
if b {
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user