package op import ( "errors" "fmt" "hash/crc32" "sync/atomic" "github.com/gorilla/websocket" "github.com/synctv-org/synctv/internal/db" "github.com/synctv-org/synctv/internal/model" "github.com/synctv-org/synctv/internal/settings" "github.com/synctv-org/synctv/utils" "github.com/zijiren233/gencontainer/rwmap" rtmps "github.com/zijiren233/livelib/server" "github.com/zijiren233/stream" "golang.org/x/crypto/bcrypt" ) type Room struct { model.Room version uint32 current *current initOnce utils.Once hub *Hub movies movies members rwmap.RWMap[string, *model.RoomMember] } func (r *Room) lazyInitHub() { r.initOnce.Do(func() { r.hub = newHub(r.ID) }) } func (r *Room) PeopleNum() int64 { if r.hub == nil { return 0 } return r.hub.PeopleNum() } func (r *Room) KickUser(userID string) error { if r.hub == nil { return nil } return r.hub.KickUser(userID) } func (r *Room) Broadcast(data Message, conf ...BroadcastConf) error { if r.hub == nil { return nil } return r.hub.Broadcast(data, conf...) } func (r *Room) SendToUser(user *User, data Message) error { if r.hub == nil { return nil } return r.hub.SendToUser(user.ID, data) } func (r *Room) GetChannel(channelName string) (*rtmps.Channel, error) { return r.movies.GetChannel(channelName) } func (r *Room) close() { if r.initOnce.Done() { r.hub.Close() r.movies.Close() } } func (r *Room) Version() uint32 { return atomic.LoadUint32(&r.version) } func (r *Room) CheckVersion(version uint32) bool { return atomic.LoadUint32(&r.version) == version } func (r *Room) UpdateMovie(movieId string, movie *model.BaseMovie) error { if r.current.current.MovieID == movieId { return errors.New("cannot update current movie") } return r.movies.Update(movieId, movie) } func (r *Room) AddMovie(m *model.Movie) error { m.RoomID = r.ID return r.movies.AddMovie(m) } func (r *Room) AddMovies(movies []*model.Movie) error { for _, m := range movies { m.RoomID = r.ID } return r.movies.AddMovies(movies) } func (r *Room) UserRole(userID string) (model.RoomMemberRole, error) { if r.IsCreator(userID) { return model.RoomMemberRoleCreator, nil } rur, err := r.LoadRoomMember(userID) if err != nil { return model.RoomMemberRoleUnknown, err } return rur.Role, nil } // do not use this value for permission determination func (r *Room) IsAdmin(userID string) bool { role, err := r.UserRole(userID) if err != nil { return false } return role.IsAdmin() } func (r *Room) IsCreator(userID string) bool { return r.CreatorID == userID } func (r *Room) IsGuest(userID string) bool { return userID == db.GuestUserID } func (r *Room) HasPermission(userID string, permission model.RoomMemberPermission) bool { if r.IsCreator(userID) { return true } rur, err := r.LoadOrCreateRoomMember(userID) if err != nil { return false } if rur.Role.IsAdmin() { return true } switch { case permission.Has(model.PermissionGetMovieList) && !r.Settings.CanGetMovieList, permission.Has(model.PermissionAddMovie) && !r.Settings.CanAddMovie, permission.Has(model.PermissionDeleteMovie) && !r.Settings.CanDeleteMovie, permission.Has(model.PermissionEditMovie) && !r.Settings.CanEditMovie, permission.Has(model.PermissionSetCurrentMovie) && !r.Settings.CanSetCurrentMovie, permission.Has(model.PermissionSetCurrentStatus) && !r.Settings.CanSetCurrentStatus, permission.Has(model.PermissionSendChatMessage) && !r.Settings.CanSendChatMessage: return false default: return rur.Permissions.Has(permission) } } func (r *Room) HasAdminPermission(userID string, permission model.RoomAdminPermission) bool { if r.IsCreator(userID) { return true } rur, err := r.LoadOrCreateRoomMember(userID) if err != nil { return false } return rur.HasAdminPermission(permission) } func (r *Room) LoadOrCreateMemberStatus(userID string) (model.RoomMemberStatus, error) { if r.IsCreator(userID) { return model.RoomMemberStatusActive, nil } rur, err := r.LoadOrCreateRoomMember(userID) if err != nil { return model.RoomMemberStatusUnknown, err } return rur.Status, nil } func (r *Room) LoadMemberStatus(userID string) (model.RoomMemberStatus, error) { if r.IsCreator(userID) { return model.RoomMemberStatusActive, nil } rur, err := r.LoadRoomMember(userID) if err != nil { return model.RoomMemberStatusUnknown, err } return rur.Status, nil } func (r *Room) LoadOrCreateRoomMember(userID string) (*model.RoomMember, error) { if r.Settings.DisableJoinNewUser { return r.LoadRoomMember(userID) } if r.IsGuest(userID) && (r.Settings.DisableGuest || !settings.EnableGuest.Get()) { return nil, errors.New("guest is disabled") } member, ok := r.members.Load(userID) if ok { return member, nil } var conf []db.CreateRoomMemberRelationConfig if r.IsCreator(userID) { conf = append( conf, db.WithRoomMemberStatus(model.RoomMemberStatusActive), db.WithRoomMemberPermissions(model.AllPermissions), db.WithRoomMemberRole(model.RoomMemberRoleCreator), db.WithRoomMemberAdminPermissions(model.AllAdminPermissions), ) } else { if r.IsGuest(userID) { conf = append( conf, db.WithRoomMemberPermissions(model.NoPermission), db.WithRoomMemberRole(model.RoomMemberRoleMember), db.WithRoomMemberAdminPermissions(model.NoAdminPermission), ) } else { conf = append( conf, db.WithRoomMemberPermissions(r.Settings.UserDefaultPermissions), db.WithRoomMemberRole(model.RoomMemberRoleMember), db.WithRoomMemberAdminPermissions(model.NoAdminPermission), ) } if r.Settings.JoinNeedReview { conf = append(conf, db.WithRoomMemberStatus(model.RoomMemberStatusPending)) } else { conf = append(conf, db.WithRoomMemberStatus(model.RoomMemberStatusActive)) } } member, err := db.FirstOrCreateRoomMemberRelation(r.ID, userID, conf...) if err != nil { return nil, err } if r.IsCreator(userID) { member.Role = model.RoomMemberRoleCreator member.Permissions = model.AllPermissions member.AdminPermissions = model.AllAdminPermissions member.Status = model.RoomMemberStatusActive } else if r.IsGuest(userID) { member.Role = model.RoomMemberRoleMember member.Permissions = model.NoPermission member.AdminPermissions = model.NoAdminPermission if member.Status.IsBanned() { member.Status = model.RoomMemberStatusActive } } else if member.Role.IsAdmin() { member.Permissions = model.AllPermissions } member, _ = r.members.LoadOrStore(userID, member) return member, nil } func (r *Room) LoadRoomMember(userID string) (*model.RoomMember, error) { if r.IsGuest(userID) && (r.Settings.DisableGuest || !settings.EnableGuest.Get()) { return nil, errors.New("guest is disabled") } member, ok := r.members.Load(userID) if ok { return member, nil } member, err := db.GetRoomMember(r.ID, userID) if err != nil { return nil, fmt.Errorf("get room member failed: %w", err) } if r.IsCreator(userID) { member.Role = model.RoomMemberRoleCreator member.Permissions = model.AllPermissions member.AdminPermissions = model.AllAdminPermissions member.Status = model.RoomMemberStatusActive } else if r.IsGuest(userID) { member.Role = model.RoomMemberRoleMember member.Permissions = model.NoPermission member.AdminPermissions = model.NoAdminPermission if member.Status.IsBanned() { member.Status = model.RoomMemberStatusActive } } else if member.Role.IsAdmin() { member.Permissions = model.AllPermissions } member, _ = r.members.LoadOrStore(userID, member) return member, nil } func (r *Room) LoadRoomMemberPermission(userID string) (model.RoomMemberPermission, error) { if r.IsCreator(userID) { return model.AllPermissions, nil } member, err := r.LoadRoomMember(userID) if err != nil { return model.NoPermission, err } return member.Permissions, nil } func (r *Room) LoadRoomAdminPermission(userID string) (model.RoomAdminPermission, error) { if r.IsCreator(userID) { return model.AllAdminPermissions, nil } member, err := r.LoadRoomMember(userID) if err != nil { return model.NoAdminPermission, err } return member.AdminPermissions, nil } func (r *Room) NeedPassword() bool { return len(r.HashedPassword) != 0 } func (r *Room) SetPassword(password string) error { if r.CheckPassword(password) && r.NeedPassword() { return errors.New("password is the same") } var hashedPassword []byte if password != "" { var err error hashedPassword, err = bcrypt.GenerateFromPassword(stream.StringToBytes(password), bcrypt.DefaultCost) if err != nil { return err } atomic.StoreUint32(&r.version, crc32.ChecksumIEEE(hashedPassword)) } r.HashedPassword = hashedPassword return db.SetRoomHashedPassword(r.ID, hashedPassword) } func (r *Room) GetMoviesCount() int { return r.movies.Len() } func (r *Room) DeleteMovieByID(id string) error { if r.current.current.MovieID == id { return errors.New("cannot delete current movie") } return r.movies.DeleteMovieByID(id) } func (r *Room) DeleteMoviesByID(ids []string) error { if r.current.current.MovieID != "" { for _, id := range ids { if id == r.current.current.MovieID { return errors.New("cannot delete current movie") } } } return r.movies.DeleteMoviesByID(ids) } func (r *Room) ClearMovies() error { _ = r.SetCurrentMovie("", false) return r.movies.Clear() } func (r *Room) GetMovieByID(id string) (*Movie, error) { return r.movies.GetMovieByID(id) } func (r *Room) Current() *Current { c := r.current.Current() return &c } func (r *Room) CurrentMovieID() string { return r.current.current.MovieID } var ErrNoCurrentMovie = errors.New("no current movie") func (r *Room) CurrentMovie() (*Movie, error) { if r.current.current.MovieID == "" { return nil, ErrNoCurrentMovie } return r.GetMovieByID(r.current.current.MovieID) } func (r *Room) CheckCurrentExpired(expireId uint64) (bool, error) { m, err := r.CurrentMovie() if err != nil { return false, err } return m.CheckExpired(expireId), nil } func (r *Room) SetCurrentMovie(movieID string, play bool) error { currentMovie, err := r.CurrentMovie() if err != nil { if err != ErrNoCurrentMovie { return err } } else { err = currentMovie.ClearCache() if err != nil { return fmt.Errorf("clear cache failed: %w", err) } } if movieID == "" { r.current.SetMovie("", false, play) return nil } m, err := r.GetMovieByID(movieID) if err != nil { return err } r.current.SetMovie(m.ID, m.Base.Live, play) return nil } func (r *Room) SwapMoviePositions(id1, id2 string) error { return r.movies.SwapMoviePositions(id1, id2) } func (r *Room) GetMoviesWithPage(page, pageSize int, creator string) ([]*Movie, int) { return r.movies.GetMoviesWithPage(page, pageSize, creator) } func (r *Room) NewClient(user *User, conn *websocket.Conn) (*Client, error) { r.lazyInitHub() cli := newClient(user, r, conn) err := r.hub.RegClient(cli) if err != nil { return nil, err } return cli, nil } func (r *Room) RegClient(cli *Client) error { r.lazyInitHub() return r.hub.RegClient(cli) } func (r *Room) UnregisterClient(cli *Client) error { r.lazyInitHub() return r.hub.UnRegClient(cli) } func (r *Room) UserIsOnline(userID string) bool { r.lazyInitHub() return r.hub.IsOnline(userID) } func (r *Room) UserOnlineCount(userID string) int { r.lazyInitHub() return r.hub.OnlineCount(userID) } func (r *Room) SetCurrentStatus(playing bool, seek float64, rate float64, timeDiff float64) *Status { return r.current.SetStatus(playing, seek, rate, timeDiff) } func (r *Room) SetCurrentSeekRate(seek float64, rate float64, timeDiff float64) *Status { return r.current.SetSeekRate(seek, rate, timeDiff) } func (r *Room) SetSettings(settings *model.RoomSettings) error { err := db.SaveRoomSettings(r.ID, settings) if err != nil { return err } r.Settings = settings if settings.DisableGuest { return r.KickUser(db.GuestUserID) } return nil } func (r *Room) UpdateSettings(settings map[string]any) error { rs, err := db.UpdateRoomSettings(r.ID, settings) if err != nil { return err } r.Settings = rs if rs.DisableGuest { return r.KickUser(db.GuestUserID) } return nil } func (r *Room) ResetMemberPermissions(userID string) error { return r.SetMemberPermissions(userID, r.Settings.UserDefaultPermissions) } func (r *Room) SetMemberPermissions(userID string, permissions model.RoomMemberPermission) error { if r.IsCreator(userID) { return errors.New("you are creator, cannot set permissions") } if r.IsGuest(userID) { return errors.New("cannot set permissions to guest") } defer r.members.Delete(userID) return db.SetMemberPermissions(r.ID, userID, permissions) } func (r *Room) AddMemberPermissions(userID string, permissions model.RoomMemberPermission) error { if r.IsGuest(userID) { return errors.New("cannot add permissions to guest") } if r.IsAdmin(userID) { return errors.New("cannot add permissions to admin") } defer r.members.Delete(userID) return db.AddMemberPermissions(r.ID, userID, permissions) } func (r *Room) RemoveMemberPermissions(userID string, permissions model.RoomMemberPermission) error { if r.IsAdmin(userID) { return errors.New("cannot remove permissions from admin") } defer r.members.Delete(userID) return db.RemoveMemberPermissions(r.ID, userID, permissions) } func (r *Room) ApprovePendingMember(userID string) error { if r.IsCreator(userID) { return errors.New("you are creator, cannot approve") } defer r.members.Delete(userID) return db.RoomApprovePendingMember(r.ID, userID) } func (r *Room) BanMember(userID string) error { if r.IsCreator(userID) { return errors.New("you are creator, cannot ban") } if r.IsGuest(userID) { return errors.New("cannot ban guest") } defer func() { r.members.Delete(userID) _ = r.KickUser(userID) }() return db.RoomBanMember(r.ID, userID) } func (r *Room) UnbanMember(userID string) error { if r.IsCreator(userID) { return errors.New("you are creator, cannot unban") } defer r.members.Delete(userID) return db.RoomUnbanMember(r.ID, userID) } func (r *Room) ResetAdminPermissions(userID string) error { return r.SetAdminPermissions(userID, model.DefaultAdminPermissions) } func (r *Room) SetAdminPermissions(userID string, permissions model.RoomAdminPermission) error { if r.IsCreator(userID) { return errors.New("you are creator, cannot set admin permissions") } if r.IsGuest(userID) { return errors.New("cannot set admin permissions to guest") } if member, err := r.LoadRoomMember(userID); err != nil { return err } else if !member.Role.IsAdmin() { return errors.New("not admin") } defer r.members.Delete(userID) return db.RoomSetAdminPermissions(r.ID, userID, permissions) } func (r *Room) AddAdminPermissions(userID string, permissions model.RoomAdminPermission) error { if r.IsCreator(userID) { return errors.New("you are creator, cannot add admin permissions") } if r.IsGuest(userID) { return errors.New("cannot add admin permissions to guest") } if member, err := r.LoadRoomMember(userID); err != nil { return err } else if !member.Role.IsAdmin() { return errors.New("not admin") } defer r.members.Delete(userID) return db.RoomSetAdminPermissions(r.ID, userID, permissions) } func (r *Room) RemoveAdminPermissions(userID string, permissions model.RoomAdminPermission) error { if r.IsCreator(userID) { return errors.New("you are creator, cannot remove admin permissions") } if r.IsGuest(userID) { return errors.New("cannot remove admin permissions from guest") } if member, err := r.LoadRoomMember(userID); err != nil { return err } else if !member.Role.IsAdmin() { return errors.New("not admin") } defer r.members.Delete(userID) return db.RoomSetAdminPermissions(r.ID, userID, 0) } func (r *Room) SetAdmin(userID string, permissions model.RoomAdminPermission) error { if r.IsCreator(userID) { return errors.New("you are creator, cannot set admin") } if r.IsGuest(userID) { return errors.New("cannot set guest as admin") } defer r.members.Delete(userID) return db.RoomSetAdmin(r.ID, userID, permissions) } func (r *Room) SetMember(userID string, permissions model.RoomMemberPermission) error { if r.IsCreator(userID) { return errors.New("you are creator, cannot set member") } defer r.members.Delete(userID) return db.RoomSetMember(r.ID, userID, permissions) }