146 lines
4.6 KiB
Go

package service
import (
"context"
"strings"
"time"
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
roomv1 "hyapp.local/api/proto/room/v1"
"hyapp/pkg/tencentim"
"hyapp/pkg/xerr"
"hyapp/services/room-service/internal/room/command"
"hyapp/services/room-service/internal/room/outbox"
"hyapp/services/room-service/internal/room/rank"
"hyapp/services/room-service/internal/room/state"
)
const defaultSystemEvictReason = "platform_moderation"
// SystemEvictUser removes a platform-banned or risk-blocked user from their current Room Cell.
// It deliberately bypasses room manager role checks, but still serializes through the Room Cell command log.
func (s *Service) SystemEvictUser(ctx context.Context, req *roomv1.SystemEvictUserRequest) (*roomv1.SystemEvictUserResponse, error) {
ctx = contextFromMeta(ctx, req.GetMeta())
targetUserID := req.GetTargetUserId()
if targetUserID <= 0 {
return nil, xerr.New(xerr.InvalidArgument, "target_user_id is required")
}
roomID := strings.TrimSpace(req.GetMeta().GetRoomId())
if roomID == "" {
presence, exists, err := s.repository.GetCurrentRoomPresence(ctx, targetUserID)
if err != nil {
return nil, err
}
if !exists {
return &roomv1.SystemEvictUserResponse{
HadCurrentRoom: false,
Result: commandResult(false, 0, s.clock.Now()),
}, nil
}
roomID = presence.RoomID
}
base := baseFromMeta(req.GetMeta())
base.Room = roomID
if base.ActorID <= 0 {
// mutateRoom requires a non-zero actor for idempotency metadata; system eviction does not use it for authorization.
base.ActorID = targetUserID
}
cmd := command.SystemEvictUser{
Base: base,
TargetUserID: targetUserID,
OperatorUserID: req.GetOperatorUserId(),
Reason: normalizeSystemEvictReason(req.GetReason()),
BanFromRoom: req.GetBanFromRoom(),
}
shouldKickRTC := false
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
if err := requireActiveRoom(current); err != nil {
return mutationResult{}, nil, err
}
_, inRoom := current.OnlineUsers[cmd.TargetUserID]
_, onSeat := current.SeatByUser(cmd.TargetUserID)
alreadyBanned := current.BanUsers[cmd.TargetUserID]
if !inRoom && !onSeat && !current.AdminUsers[cmd.TargetUserID] && (!cmd.BanFromRoom || alreadyBanned) {
// 重复系统驱逐保持幂等;没有残留状态时只返回当前快照。
return mutationResult{snapshot: current.ToProto()}, nil, nil
}
shouldKickRTC = inRoom || onSeat
var releasedSeatNo int32
var releasedMicSessionID string
if seat, exists := current.SeatByUser(cmd.TargetUserID); exists {
index := current.SeatIndex(seat.SeatNo)
releasedSeatNo = seat.SeatNo
releasedMicSessionID = current.MicSeats[index].MicSessionID
current.ClearMicSession(index)
}
delete(current.OnlineUsers, cmd.TargetUserID)
delete(current.AdminUsers, cmd.TargetUserID)
if cmd.BanFromRoom {
current.BanUsers[cmd.TargetUserID] = true
}
current.Version++
evictEvent, err := outbox.Build(current.RoomID, "RoomUserKicked", current.Version, now, &roomeventsv1.RoomUserKicked{
ActorUserId: 0,
TargetUserId: cmd.TargetUserID,
})
if err != nil {
return mutationResult{}, nil, err
}
records := make([]outbox.Record, 0, 2)
if releasedMicSessionID != "" {
micEvent, err := buildMicDownEventForSeat(current.RoomID, current.Version, now, 0, cmd.TargetUserID, releasedSeatNo, releasedMicSessionID, cmd.Reason)
if err != nil {
return mutationResult{}, nil, err
}
records = append(records, micEvent)
}
records = append(records, evictEvent)
return mutationResult{
snapshot: current.ToProto(),
syncEvent: &tencentim.RoomEvent{
EventID: evictEvent.EventID,
RoomID: current.RoomID,
EventType: "room_user_kicked",
TargetUserID: cmd.TargetUserID,
RoomVersion: current.Version,
Attributes: map[string]string{"reason": cmd.Reason},
},
}, records, nil
})
if err != nil {
return nil, err
}
response := &roomv1.SystemEvictUserResponse{
HadCurrentRoom: true,
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
Room: result.snapshot,
RoomId: roomID,
}
if result.applied && shouldKickRTC && s.rtcUserRemover != nil {
if err := s.rtcUserRemover.RemoveUserByStrRoomID(ctx, roomID, targetUserID); err != nil {
response.RtcKickError = err.Error()
} else {
response.RtcKicked = true
}
}
return response, nil
}
func normalizeSystemEvictReason(reason string) string {
reason = strings.TrimSpace(reason)
if reason == "" {
return defaultSystemEvictReason
}
return reason
}