372 lines
11 KiB
Go
372 lines
11 KiB
Go
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)
|
||
}
|