package room import ( "fmt" "strings" "time" ) type RoomStatus string const ( RoomActive RoomStatus = "active" RoomClosed RoomStatus = "closed" RoomBanned RoomStatus = "banned" ) func (s RoomStatus) IsActive() bool { return s == RoomActive } func (s RoomStatus) IsClosed() bool { return s == RoomClosed || s == RoomBanned } func (s RoomStatus) CanTransitionTo(next RoomStatus) bool { if s == next { return true } switch s { case RoomActive: return next == RoomClosed || next == RoomBanned case RoomClosed: return next == RoomActive case RoomBanned: return next == RoomActive default: return false } } func ParseRoomStatus(raw string) (RoomStatus, error) { switch strings.ToLower(strings.TrimSpace(raw)) { case "active": return RoomActive, nil case "closed": return RoomClosed, nil case "banned": return RoomBanned, nil default: return "", fmt.Errorf("unknown room status %q", raw) } } type PlayMode string const ( PlaySequential PlayMode = "sequential" PlayRepeatOne PlayMode = "repeat_one" PlayRepeatAll PlayMode = "repeat_all" PlayShuffle PlayMode = "shuffle" ) func ParsePlayMode(s string) (PlayMode, error) { switch strings.ToLower(strings.TrimSpace(s)) { case "sequential", "": return PlaySequential, nil case "repeat_one", "repeatone": return PlayRepeatOne, nil case "repeat_all", "repeatall": return PlayRepeatAll, nil case "shuffle", "random": return PlayShuffle, nil default: return "", fmt.Errorf("unknown play mode %q", s) } } type AutoPlaySettings struct { Enabled bool `json:"enabled"` Mode PlayMode `json:"mode"` Delay uint32 `json:"delay"` // seconds } func DefaultAutoPlay() AutoPlaySettings { return AutoPlaySettings{Enabled: true, Mode: PlaySequential, Delay: 3} } type RoomCategory struct { ID int64 `json:"id"` Key string `json:"key"` Name string `json:"name"` Description string `json:"description"` SortOrder int32 `json:"sort_order"` IsEnabled bool `json:"is_enabled"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type RoomLabel struct { ID int64 `json:"id"` Key string `json:"key"` Name string `json:"name"` Description string `json:"description"` Color string `json:"color"` CategoryID *int64 `json:"category_id,omitempty"` SortOrder int32 `json:"sort_order"` IsEnabled bool `json:"is_enabled"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type Room struct { Name string `json:"name"` DisplayName string `json:"display_name,omitempty"` Description string `json:"description,omitempty"` Creator string `json:"creator"` Status RoomStatus `json:"status"` IsPublic bool `json:"is_public"` PasswordHash string `json:"-"` RequireApproval bool `json:"require_approval"` MaxMembers int `json:"max_members"` MemberCount int `json:"member_count"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` ClosedAt *time.Time `json:"closed_at,omitempty"` Settings *RoomSettings `json:"settings,omitempty"` Version int64 `json:"version"` CategoryID *int64 `json:"category_id,omitempty"` LabelIDs []int64 `json:"label_ids,omitempty"` CoverFileID *string `json:"cover_file_id,omitempty"` AutoPlay AutoPlaySettings `json:"auto_play"` } type RoomWithCount struct { Room `json:",inline"` MemberCount int `json:"member_count"` } type SortDirection string const ( SortAsc SortDirection = "asc" SortDesc SortDirection = "desc" ) type PageParams struct { Page int `json:"page"` PageSize int `json:"page_size"` } func (p PageParams) Offset() int { if p.Page <= 1 { return 0 } return (p.Page - 1) * p.Limit() } func (p PageParams) Limit() int { if p.PageSize <= 0 { return 20 } if p.PageSize > 100 { return 100 } return p.PageSize } type RoomListSortBy string const ( SortByCreatedAt RoomListSortBy = "created_at" SortByUpdatedAt RoomListSortBy = "updated_at" SortByName RoomListSortBy = "name" SortByMemberCount RoomListSortBy = "member_count" ) func (s RoomListSortBy) AsSQL() string { switch s { case SortByName: return "name" case SortByMemberCount: return "member_count" case SortByUpdatedAt: return "updated_at" default: return "created_at" } } type RoomListQuery struct { Pagination PageParams `json:"pagination"` Status *RoomStatus `json:"status,omitempty"` Search *string `json:"search,omitempty"` IsBanned *bool `json:"is_banned,omitempty"` IsPublic *bool `json:"is_public,omitempty"` Creator *string `json:"creator,omitempty"` CategoryID *int64 `json:"category_id,omitempty"` LabelIDs []int64 `json:"label_ids,omitempty"` SortBy RoomListSortBy `json:"sort_by"` SortDirection SortDirection `json:"sort_direction"` } func DefaultRoomListQuery() RoomListQuery { active := RoomActive return RoomListQuery{ Pagination: PageParams{Page: 1, PageSize: 20}, Status: &active, SortBy: SortByCreatedAt, SortDirection: SortDesc, } } func NewRoom(name, creator string) *Room { now := time.Now() return &Room{ Name: name, DisplayName: name, Creator: creator, Status: RoomActive, IsPublic: true, MaxMembers: 100, CreatedAt: now, UpdatedAt: now, Version: 1, Settings: DefaultSettings(), AutoPlay: DefaultAutoPlay(), } } type RoomSettings struct { IsPublic bool `json:"is_public"` RequireApproval bool `json:"require_approval"` AllowGuestJoin bool `json:"allow_guest_join"` ChatEnabled bool `json:"chat_enabled"` MaxMembers int `json:"max_members"` AllowAutoJoin bool `json:"allow_auto_join"` AdminAddedPerms uint64 `json:"admin_added_permissions"` AdminRemovedPerms uint64 `json:"admin_removed_permissions"` MemberAddedPerms uint64 `json:"member_added_permissions"` MemberRemovedPerms uint64 `json:"member_removed_permissions"` GuestAddedPerms uint64 `json:"guest_added_permissions"` GuestRemovedPerms uint64 `json:"guest_removed_permissions"` AutoPlayEnabled bool `json:"auto_play_enabled"` PlayMode PlayMode `json:"play_mode"` Distributors *string `json:"distributors,omitempty"` } // Distributors helpers: per-room distribution override with fallback to global. // HasCustomDistributors returns true when room explicitly sets distributors (even if empty string means disable all). func (s *RoomSettings) HasCustomDistributors() bool { return s != nil && s.Distributors != nil } // EffectiveDistributors returns parsed per-room distributors if set, otherwise fallback to global. func (s *RoomSettings) EffectiveDistributors(global []string) []string { if s != nil && s.Distributors != nil { return ParseDistributors(*s.Distributors) } return global } // SetDistributors sets the per-room distributors override. Use nil to clear (fallback to global). func (s *RoomSettings) SetDistributors(v *string) { s.Distributors = v } // ParseDistributors parses a comma-separated distributors string into normalized list. // Supported values: srs-hls (alias srs/hls), cf (alias cloudflare/cf-sfu), cdn. // Empty input yields empty list. func ParseDistributors(raw string) []string { if raw == "" { return []string{} } parts := strings.Split(raw, ",") out := make([]string, 0, len(parts)) seen := make(map[string]struct{}, len(parts)) for _, p := range parts { p = strings.TrimSpace(strings.ToLower(p)) if p == "" { continue } if p == "srs" || p == "hls" { p = "srs-hls" } if p == "cloudflare" || p == "cf-sfu" { p = "cf" } if p != "srs-hls" && p != "cf" && p != "cdn" { continue } if _, ok := seen[p]; !ok { seen[p] = struct{}{} out = append(out, p) } } return out } func NormalizeDistributorsString(raw string) string { return strings.Join(ParseDistributors(raw), ",") } func ValidateDistributorsList(list []string) error { for _, d := range list { if d != "srs-hls" && d != "cf" && d != "cdn" { return fmt.Errorf("unsupported distributor %q", d) } } return nil } func DefaultSettings() *RoomSettings { return &RoomSettings{ IsPublic: true, AllowGuestJoin: false, ChatEnabled: true, MaxMembers: 100, AllowAutoJoin: true, RequireApproval: false, AutoPlayEnabled: true, PlayMode: PlaySequential, } } func (s *RoomSettings) AdminPermissions() PermissionSet { base := PermDefaultAdmin base |= PermissionSet(s.AdminAddedPerms) base &^= PermissionSet(s.AdminRemovedPerms) return base } func (s *RoomSettings) MemberPermissions() PermissionSet { base := PermDefaultMember base |= PermissionSet(s.MemberAddedPerms) base &^= PermissionSet(s.MemberRemovedPerms) return base } func (s *RoomSettings) GuestPermissions() PermissionSet { base := PermDefaultGuest base |= PermissionSet(s.GuestAddedPerms) base &^= PermissionSet(s.GuestRemovedPerms) return base & PermGuestAssignable } func (s *RoomSettings) IsChatEnabled() bool { return s.ChatEnabled } func (s *RoomSettings) EffectiveAutoPlay() AutoPlaySettings { enabled := s.AutoPlayEnabled mode := s.PlayMode if mode == "" { mode = PlaySequential } return AutoPlaySettings{Enabled: enabled, Mode: mode, Delay: 3} }