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

372 lines
11 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package room
import (
"context"
"database/sql"
"errors"
"golang.org/x/crypto/bcrypt"
)
type Service struct {
store *Store
}
func NewService(store *Store) *Service { return &Service{store: store} }
func (s *Service) Store() *Store { return s.store }
func (s *Service) GetRoom(ctx context.Context, name string) (*Room, error) {
if s.store == nil || s.store.db == nil {
return nil, errors.New("room store not configured")
}
return s.store.GetRoom(ctx, name)
}
func (s *Service) CreateRoom(ctx context.Context, name, creator string, settings *RoomSettings) (*Room, error) {
if name == "" {
return nil, errors.New("room name required")
}
if creator == "" {
return nil, errors.New("creator required")
}
r := NewRoom(name, creator)
if settings != nil {
r.Settings = settings
r.IsPublic = settings.IsPublic
r.RequireApproval = settings.RequireApproval
if settings.MaxMembers > 0 {
r.MaxMembers = settings.MaxMembers
}
}
if err := s.store.CreateRoom(ctx, r); err != nil {
return nil, err
}
creatorMember := NewMember(name, creator, RoleCreator)
if err := s.store.UpsertMember(ctx, creatorMember); err != nil {
return nil, err
}
return r, nil
}
func (s *Service) JoinRoom(ctx context.Context, roomName, username string, password string) (*RoomMember, error) {
r, err := s.store.GetRoom(ctx, roomName)
if err != nil {
return nil, err
}
if r.Status == RoomBanned {
return nil, errors.New("room is banned")
}
if r.Status == RoomClosed {
return nil, errors.New("room is closed")
}
if r.PasswordHash != "" {
if err := verifyRoomPassword(password, r.PasswordHash); err != nil {
return nil, err
}
}
if cnt, _ := s.store.CountMembers(ctx, roomName); cnt >= r.MaxMembers && r.MaxMembers > 0 {
return nil, errors.New("room is full")
}
member, err := s.store.GetMember(ctx, roomName, username)
if err == nil && member.Status == MemberActive {
return member, nil
}
if err == nil && member.Status == MemberBanned {
return nil, errors.New("you are banned from this room")
}
if err == nil && member.Status == MemberPending {
return nil, errors.New("join request pending approval")
}
newMember := NewMember(roomName, username, RoleMember)
if r.RequireApproval {
newMember.Status = MemberPending
}
if s.store != nil {
_ = s.store.UpsertMember(ctx, newMember)
}
return newMember, nil
}
func (s *Service) GetMember(ctx context.Context, room, userID string) (*RoomMember, error) {
return s.store.GetMember(ctx, room, userID)
}
func (s *Service) EnsurePermission(ctx context.Context, room, userID string, perm RoomPermission) error {
if s.store == nil || s.store.db == nil {
return nil
}
r, err := s.store.GetRoom(ctx, room)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return errors.New("room not found")
}
return err
}
if r.Status == RoomBanned {
return errors.New("room is banned")
}
if r.Status == RoomClosed && perm != PermViewMembers && perm != PermViewChatHistory {
return errors.New("room is closed")
}
m, err := s.store.GetMember(ctx, room, userID)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
if r.IsPublic || r.Settings.AllowGuestJoin {
guest := NewMember(room, userID, RoleGuest)
if guest.HasPermission(perm, r.Settings) {
return nil
}
}
return errors.New("not a member of this room")
}
return err
}
if m.Status != MemberActive {
return errors.New("member not active")
}
if !m.HasPermission(perm, r.Settings) {
return errors.New("permission denied: " + perm.String())
}
return nil
}
func (s *Service) CheckPermission(ctx context.Context, room, userID string, perm RoomPermission) bool {
return s.EnsurePermission(ctx, room, userID, perm) == nil
}
func (s *Service) ListMembers(ctx context.Context, room string) ([]*RoomMember, error) {
return s.store.ListMembers(ctx, room)
}
func (s *Service) UpdateMemberRole(ctx context.Context, room, actorID, targetID string, newRole RoomRole) error {
if newRole == RoleCreator {
return errors.New("cannot assign creator role")
}
roomObj, err := s.store.GetRoom(ctx, room)
if err != nil {
return err
}
actor, err := s.store.GetMember(ctx, room, actorID)
if err != nil {
return errors.New("actor not in room")
}
if !actor.HasPermission(PermManageMembers, roomObj.Settings) {
return errors.New("permission denied: manage members")
}
if !actor.Role.CanManage(newRole) {
return errors.New("cannot manage role higher than yours")
}
target, err := s.store.GetMember(ctx, room, targetID)
if err != nil {
return errors.New("target not in room")
}
if target.Role == RoleCreator {
return errors.New("cannot change creator role")
}
if !actor.Role.CanManage(target.Role) {
return errors.New("cannot manage member with higher or equal role")
}
target.Role = newRole
return s.store.UpsertMember(ctx, target)
}
func (s *Service) UpdateMemberPermissions(ctx context.Context, room, actorID, targetID string, added, removed uint64, isAdmin bool) error {
roomObj, err := s.store.GetRoom(ctx, room)
if err != nil {
return err
}
actor, err := s.store.GetMember(ctx, room, actorID)
if err != nil {
return errors.New("actor not in room")
}
if !actor.HasPermission(PermManagePermissions, roomObj.Settings) {
return errors.New("permission denied: manage permissions")
}
target, err := s.store.GetMember(ctx, room, targetID)
if err != nil {
return errors.New("target not in room")
}
if !actor.Role.CanManage(target.Role) {
return errors.New("cannot manage this member")
}
if isAdmin {
if target.Role != RoleAdmin && target.Role != RoleCreator {
return errors.New("admin permissions only for admin/creator")
}
target.AdminAdded = added
target.AdminRemoved = removed
} else {
target.AddedPerms = added
target.RemovedPerms = removed
}
return s.store.UpsertMember(ctx, target)
}
func (s *Service) KickMember(ctx context.Context, room, actorID, targetID string) error {
roomObj, err := s.store.GetRoom(ctx, room)
if err != nil {
return err
}
actor, err := s.store.GetMember(ctx, room, actorID)
if err != nil {
return errors.New("actor not in room")
}
if !actor.HasPermission(PermRemoveMembers, roomObj.Settings) {
return errors.New("permission denied: remove members")
}
target, err := s.store.GetMember(ctx, room, targetID)
if err != nil {
return errors.New("target not in room")
}
if target.Role == RoleCreator {
return errors.New("cannot kick creator")
}
if !actor.Role.CanManage(target.Role) {
return errors.New("cannot kick higher role")
}
return s.store.RemoveMember(ctx, room, targetID)
}
func (s *Service) BanMember(ctx context.Context, room, actorID, targetID string) error {
if err := s.KickMember(ctx, room, actorID, targetID); err != nil {
return err
}
banned := NewMember(room, targetID, RoleGuest)
banned.Status = MemberBanned
return s.store.UpsertMember(ctx, banned)
}
func (s *Service) LeaveRoom(ctx context.Context, room, userID string) error {
m, err := s.store.GetMember(ctx, room, userID)
if err != nil {
return err
}
if m.Role == RoleCreator {
return errors.New("creator cannot leave, transfer ownership or delete room")
}
return s.store.RemoveMember(ctx, room, userID)
}
func (s *Service) UpdateRoomSettings(ctx context.Context, room, actorID string, newSettings *RoomSettings) error {
r, err := s.store.GetRoom(ctx, room)
if err != nil {
return err
}
actor, err := s.store.GetMember(ctx, room, actorID)
if err != nil {
return errors.New("actor not in room")
}
if !actor.HasPermission(PermManageRoom, r.Settings) {
return errors.New("permission denied: manage room")
}
return s.store.UpdateRoomSettings(ctx, room, newSettings)
}
// HashRoomPassword 使用 bcrypt 哈希房间密码。
func HashRoomPassword(password string) (string, error) {
if password == "" {
return "", nil
}
b, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(b), nil
}
func verifyRoomPassword(input, hash string) error {
if hash == "" {
return nil
}
if err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(input)); err != nil {
return errors.New("invalid room password")
}
return nil
}
// ApproveJoin 将 pending 成员升级为 active member,需 PermManageMembers 权限。
func (s *Service) ApproveJoin(ctx context.Context, room, actorID, targetID string) error {
roomObj, err := s.store.GetRoom(ctx, room)
if err != nil {
return err
}
actor, err := s.store.GetMember(ctx, room, actorID)
if err != nil {
return errors.New("actor not in room")
}
if !actor.HasPermission(PermManageMembers, roomObj.Settings) {
return errors.New("permission denied: manage members")
}
target, err := s.store.GetMember(ctx, room, targetID)
if err != nil {
return errors.New("target not found")
}
if target.Status != MemberPending {
return errors.New("member is not pending approval")
}
target.Status = MemberActive
return s.store.UpsertMember(ctx, target)
}
// RejectJoin 删除一个 pending 成员的加入请求。
func (s *Service) RejectJoin(ctx context.Context, room, actorID, targetID string) error {
roomObj, err := s.store.GetRoom(ctx, room)
if err != nil {
return err
}
actor, err := s.store.GetMember(ctx, room, actorID)
if err != nil {
return errors.New("actor not in room")
}
if !actor.HasPermission(PermManageMembers, roomObj.Settings) {
return errors.New("permission denied: manage members")
}
target, err := s.store.GetMember(ctx, room, targetID)
if err != nil {
return errors.New("target not found")
}
if target.Status != MemberPending {
return errors.New("member is not pending approval")
}
return s.store.RemoveMember(ctx, room, targetID)
}
// TransferOwnership 将房间所有权从当前 creator 转移给目标 active 成员。
func (s *Service) TransferOwnership(ctx context.Context, roomName, actorID, targetID string) error {
r, err := s.store.GetRoom(ctx, roomName)
if err != nil {
return err
}
_ = r
actor, err := s.store.GetMember(ctx, roomName, actorID)
if err != nil {
return errors.New("actor not in room")
}
if actor.Role != RoleCreator {
return errors.New("only creator can transfer ownership")
}
target, err := s.store.GetMember(ctx, roomName, targetID)
if err != nil {
return errors.New("target not in room")
}
if target.Status != MemberActive {
return errors.New("target must be an active member")
}
if target.Role == RoleCreator {
return errors.New("target is already the creator")
}
// 更新房间表的 creator 字段
if err := s.store.UpdateRoomCreator(ctx, roomName, targetID); err != nil {
return err
}
// 升级 target 为 creator
target.Role = RoleCreator
if err := s.store.UpsertMember(ctx, target); err != nil {
return err
}
// 降级 actor 为 admin
actor.Role = RoleAdmin
return s.store.UpsertMember(ctx, actor)
}