live-sfu-demo/internal/room/room.go

349 lines
9.3 KiB
Go

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}
}