Files
OwnCord/Server/db/channel_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

281 lines
8.6 KiB
Go

package db
import (
"database/sql"
"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.q.ListChannels(dbCtx())
if err != nil {
return nil, fmt.Errorf("ListChannels: %w", err)
}
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) {
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 := 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.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)
}
return res.LastInsertId()
}
// UpdateChannel modifies name, topic, and slow_mode for the given channel.
func (d *DB) UpdateChannel(id int64, name, topic string, slowMode int) error {
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
}
// SetChannelSlowMode updates only the slow_mode field for the given channel.
func (d *DB) SetChannelSlowMode(id int64, slowMode int) error {
if err := d.q.SetChannelSlowMode(dbCtx(), dbgen.SetChannelSlowModeParams{
SlowMode: int64(slowMode),
ID: id,
}); err != nil {
return fmt.Errorf("SetChannelSlowMode: %w", err)
}
return nil
}
// SetChannelVoiceMaxUsers updates the voice_max_users field for the given channel.
func (d *DB) SetChannelVoiceMaxUsers(id int64, maxUsers int) error {
if err := d.q.SetChannelVoiceMaxUsers(dbCtx(), dbgen.SetChannelVoiceMaxUsersParams{
VoiceMaxUsers: int64(maxUsers),
ID: id,
}); err != nil {
return fmt.Errorf("SetChannelVoiceMaxUsers: %w", err)
}
return nil
}
// DeleteChannel removes the channel row (cascades to messages, overrides, etc.).
func (d *DB) DeleteChannel(id int64) error {
if err := d.q.DeleteChannel(dbCtx(), id); err != nil {
return fmt.Errorf("DeleteChannel: %w", err)
}
return nil
}
// 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) {
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 r.Allow, r.Deny, nil
}
// ChannelOverride holds the allow/deny permission bits for a single channel.
type ChannelOverride struct {
Allow int64
Deny int64
}
// GetAllChannelPermissionsForRole returns all channel permission overrides for
// 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.q.GetRoleChannelPermissions(dbCtx(), roleID)
if err != nil {
return nil, fmt.Errorf("GetAllChannelPermissionsForRole: %w", 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
}
// 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 {
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
}
// 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 {
if err := d.q.DeleteChannelPermission(dbCtx(), dbgen.DeleteChannelPermissionParams{
ChannelID: channelID,
RoleID: roleID,
}); err != nil {
return fmt.Errorf("DeleteChannelOverride: %w", err)
}
return nil
}
// ChannelRoleOverride pairs a role with its (possibly zero) permission
// override on a specific channel. Permissions carries the role's base bits so
// callers can tell which roles bypass overrides via Administrator.
type ChannelRoleOverride struct {
RoleID int64 `json:"role_id"`
RoleName string `json:"role_name"`
Position int `json:"position"`
Permissions int64 `json:"permissions"`
Allow int64 `json:"allow"`
Deny int64 `json:"deny"`
}
// ListChannelRoleOverrides returns every role together with its override bits
// on the given channel (zero allow/deny when no override row exists), ordered
// by role position descending.
func (d *DB) ListChannelRoleOverrides(channelID int64) ([]ChannelRoleOverride, error) {
rows, err := d.sqlDB.Query(
`SELECT r.id, r.name, r.position, r.permissions,
COALESCE(o.allow, 0), COALESCE(o.deny, 0)
FROM roles r
LEFT JOIN channel_overrides o ON o.role_id = r.id AND o.channel_id = ?
ORDER BY r.position DESC, r.id ASC`,
channelID,
)
if err != nil {
return nil, fmt.Errorf("ListChannelRoleOverrides: %w", err)
}
defer rows.Close() //nolint:errcheck
var result []ChannelRoleOverride
for rows.Next() {
var o ChannelRoleOverride
if scanErr := rows.Scan(&o.RoleID, &o.RoleName, &o.Position, &o.Permissions, &o.Allow, &o.Deny); scanErr != nil {
return nil, fmt.Errorf("ListChannelRoleOverrides scan: %w", scanErr)
}
result = append(result, o)
}
if rows.Err() != nil {
return nil, fmt.Errorf("ListChannelRoleOverrides rows: %w", rows.Err())
}
if result == nil {
result = []ChannelRoleOverride{}
}
return result, nil
}
// ─── helpers ──────────────────────────────────────────────────────────────────
// 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) {
if len(ids) == 0 {
return map[int64]string{}, nil
}
// Build placeholders and args.
placeholders := make([]string, len(ids))
args := make([]any, len(ids))
for i, id := range ids {
placeholders[i] = "?"
args[i] = id
}
query := fmt.Sprintf( //nolint:gosec // G201: placeholder interpolation, not user input
`SELECT id, type FROM channels WHERE id IN (%s)`,
strings.Join(placeholders, ","),
)
rows, err := d.sqlDB.Query(query, args...)
if err != nil {
return nil, fmt.Errorf("GetChannelTypes query: %w", err)
}
defer func() { _ = rows.Close() }()
result := make(map[int64]string, len(ids))
for rows.Next() {
var id int64
var chType string
if err := rows.Scan(&id, &chType); err != nil {
return nil, fmt.Errorf("GetChannelTypes scan: %w", err)
}
result[id] = chType
}
return result, rows.Err()
}