package app import ( "context" "crypto/sha256" "encoding/base64" "errors" "fmt" "strconv" "strings" "time" "evening_detective_server/internal/modules/processor_jwt" "evening_detective_server/internal/modules/roles" "evening_detective_server/internal/services/screen_service" proto "evening_detective_server/proto" "github.com/google/uuid" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" ) func (s *server) CreateScreenSession(ctx context.Context, _ *proto.CreateScreenSessionReq) (*proto.CreateScreenSessionRsp, error) { token := uuid.New().String() id, err := s.screenService.Create(ctx, hashScreenToken(token)) if err != nil { return nil, status.Error(codes.Internal, "не удалось создать сессию экрана") } return &proto.CreateScreenSessionRsp{ScreenID: fmt.Sprint(id), AccessToken: token}, nil } func (s *server) GetScreenState(ctx context.Context, req *proto.GetScreenStateReq) (*proto.GetScreenStateRsp, error) { if req == nil || req.ScreenID == "" || req.AccessToken == "" { return nil, status.Error(codes.InvalidArgument, "некорректные данные экрана") } id, err := strconv.Atoi(req.ScreenID) if err != nil || id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } session, err := s.screenService.Get(ctx, id, hashScreenToken(req.AccessToken)) if errors.Is(err, screen_service.ErrScreenNotFound) { return nil, status.Error(codes.Unauthenticated, "недействительная сессия экрана") } if err != nil { return nil, status.Error(codes.Internal, "не удалось получить состояние экрана") } state := screenState(session) s.enrichScreenState(ctx, session, state) return &proto.GetScreenStateRsp{State: state}, nil } func (s *server) AdvanceScreenMusic(ctx context.Context, req *proto.AdvanceScreenMusicReq) (*proto.AdvanceScreenMusicRsp, error) { if req == nil || req.ScreenID == "" || req.AccessToken == "" { return nil, status.Error(codes.InvalidArgument, "некорректные данные экрана") } id, err := strconv.Atoi(req.ScreenID) if err != nil || id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } session, err := s.screenService.Get(ctx, id, hashScreenToken(req.AccessToken)) if errors.Is(err, screen_service.ErrScreenNotFound) { return nil, status.Error(codes.Unauthenticated, "недействительная сессия экрана") } if err != nil || session.GameID == nil { return nil, status.Error(codes.FailedPrecondition, "экран не привязан к игре") } state, err := s.playlistService.GetGame(ctx, *session.GameID) if err != nil || state.Playlist == nil || len(state.Playlist.Tracks) == 0 { return &proto.AdvanceScreenMusicRsp{State: mapMusicState(state)}, nil } next := state.TrackIndex + 1 playback := state.Playback if next >= len(state.Playlist.Tracks) { if state.Loop { next = 0 } else { next = len(state.Playlist.Tracks) - 1 playback = 1 } } updated, err := s.playlistService.SetGame(ctx, *session.GameID, state.Playlist.ID, next, 0, state.Volume, state.Loop, playback) if err != nil { return nil, status.Error(codes.Internal, "не удалось переключить трек") } return &proto.AdvanceScreenMusicRsp{State: mapMusicState(updated)}, nil } func (s *server) GetGameScreens(ctx context.Context, req *proto.GetGameScreensReq) (*proto.GetGameScreensRsp, error) { if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if req == nil || req.GameID <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор игры") } if _, err := s.gameService.GetGame(ctx, int(req.GameID)); err != nil { return nil, status.Error(codes.NotFound, "игра не найдена") } sessions, err := s.screenService.ListByGame(ctx, int(req.GameID)) if err != nil { return nil, status.Error(codes.Internal, "не удалось получить экраны игры") } states := make([]*proto.ScreenState, 0, len(sessions)) for _, session := range sessions { state := screenState(session) s.enrichScreenState(ctx, session, state) states = append(states, state) } return &proto.GetGameScreensRsp{Screens: states}, nil } func (s *server) BindScreen(ctx context.Context, req *proto.BindScreenReq) (*proto.BindScreenRsp, error) { if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if req == nil || req.ScreenID == "" || req.AccessToken == "" || req.GameID <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректные данные привязки") } id, err := strconv.Atoi(req.ScreenID) if err != nil || id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } if _, err = s.gameService.GetGame(ctx, int(req.GameID)); err != nil { return nil, status.Error(codes.NotFound, "игра не найдена") } hash := hashScreenToken(req.AccessToken) isNewBinding, err := s.screenService.Bind(ctx, id, int(req.GameID), hash, req.GetInitializeAudio()) if err != nil { if errors.Is(err, screen_service.ErrScreenNotFound) { return nil, status.Error(codes.Unauthenticated, "недействительная сессия экрана") } return nil, status.Error(codes.Internal, "не удалось привязать экран") } if isNewBinding { if err := s.screenPlanService.ApplyCurrent(ctx, int(req.GameID), id); err != nil { return nil, status.Error(codes.Internal, "не удалось применить план экранов") } } session, err := s.screenService.Get(ctx, id, hash) if err != nil { return nil, status.Error(codes.Internal, "не удалось получить состояние экрана") } state := screenState(session) s.enrichScreenState(ctx, session, state) return &proto.BindScreenRsp{State: state}, nil } func (s *server) UnbindScreen(ctx context.Context, req *proto.UnbindScreenReq) (*proto.UnbindScreenRsp, error) { if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if req == nil || req.ScreenID == "" { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } id, err := strconv.Atoi(req.ScreenID) if err != nil || id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } if err = s.screenService.Unbind(ctx, id); err != nil { if errors.Is(err, screen_service.ErrScreenNotFound) { return nil, status.Error(codes.NotFound, "экран не найден") } return nil, status.Error(codes.Internal, "не удалось отвязать экран") } return &proto.UnbindScreenRsp{}, nil } func (s *server) SetScreenSound(ctx context.Context, req *proto.SetScreenSoundReq) (*proto.SetScreenSoundRsp, error) { if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if req == nil || req.ScreenID == "" { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } id, err := strconv.Atoi(req.ScreenID) if err != nil || id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } if err = s.screenService.SetSound(ctx, id, req.SoundEnabled); err != nil { if errors.Is(err, screen_service.ErrScreenNotFound) { return nil, status.Error(codes.NotFound, "экран не найден") } return nil, status.Error(codes.Internal, "не удалось назначить экран со звуком") } return &proto.SetScreenSoundRsp{}, nil } func canManageScreens(ctx context.Context) bool { claims, ok := processor_jwt.ClaimsFromContext(ctx) if !ok || claims == nil { return false } return roles.HasRole(claims, roles.Organizer) || roles.HasRole(claims, roles.Author) } func hashScreenToken(token string) string { sum := sha256.Sum256([]byte(token)) return base64.RawURLEncoding.EncodeToString(sum[:]) } func screenState(session *screen_service.ScreenSession) *proto.ScreenState { timerState := proto.ScreenTimerState(session.TimerState) timerRemaining := session.TimerRemainingSeconds if timerState == proto.ScreenTimerState_SCREEN_TIMER_STATE_RUNNING { if session.TimerEndsAt == nil || !session.TimerEndsAt.After(time.Now()) { timerRemaining = 0 timerState = proto.ScreenTimerState_SCREEN_TIMER_STATE_FINISHED } else { timerRemaining = int64(time.Until(*session.TimerEndsAt) / time.Second) } } state := &proto.ScreenState{ ID: fmt.Sprint(session.ID), Status: "disconnected", SoundEnabled: session.Sound, Mode: proto.ScreenMode(session.Mode), StateVersion: session.Version, PositionMs: session.Position, Command: proto.ScreenCommand(session.Command), Volume: session.Volume, TimerLabel: session.TimerLabel, TimerDurationSeconds: session.TimerDurationSeconds, TimerRemainingSeconds: timerRemaining, TimerState: timerState, Text: session.Text, QrUrl: session.QRURL, TimerType: proto.ScreenTimerType(session.TimerType), } if time.Since(session.LastSeen) <= 5*time.Second { state.Status = "connected" } if session.GameID != nil { gameID := int32(*session.GameID) state.GameID = &gameID if state.Mode == proto.ScreenMode_SCREEN_MODE_UNSPECIFIED { state.Mode = proto.ScreenMode_SCREEN_MODE_COVER } } return state } func (s *server) enrichScreenState(ctx context.Context, session *screen_service.ScreenSession, state *proto.ScreenState) { if session.GameID == nil { return } if s.playlistService != nil { if music, err := s.playlistService.GetGame(ctx, *session.GameID); err == nil { state.Music = mapMusicState(music) } } game, err := s.gameService.GetGameMedia(ctx, *session.GameID) if err != nil { return } state.GameName = game.Name if game.Scenario != nil && game.Scenario.Image != nil { state.CoverImage = *game.Scenario.Image } if game.Scenario != nil && game.Scenario.Story != nil { state.BackgroundImage = game.Scenario.Story.Background if game.Scenario.Story.Introduction != nil { state.IntroAudio = game.Scenario.Story.Introduction.Audio } if game.Scenario.Story.Conclusion != nil { state.ConclusionVideo = game.Scenario.Story.Conclusion.Video } } if state.BackgroundImage == "" { state.BackgroundImage = state.CoverImage } } func (s *server) SetScreenMode(ctx context.Context, req *proto.SetScreenModeReq) (*proto.SetScreenModeRsp, error) { if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if req == nil || req.GameID <= 0 || !screen_service.ValidMode(int32(req.Mode)) { return nil, status.Error(codes.InvalidArgument, "некорректный режим экрана") } if _, err := s.gameService.GetGame(ctx, int(req.GameID)); err != nil { return nil, status.Error(codes.NotFound, "игра не найдена") } if req.Mode == proto.ScreenMode_SCREEN_MODE_QR { if !screen_service.ValidQR(req.QrUrl, req.Text) { return nil, status.Error(codes.InvalidArgument, "Укажите ссылку HTTP/HTTPS до 2048 байт и текст до 10000 байт") } if err := s.screenService.SetQR(ctx, int(req.GameID), req.QrUrl, req.Text); err != nil { return nil, status.Error(codes.Internal, "не удалось изменить QR-код") } return &proto.SetScreenModeRsp{}, nil } if err := s.screenService.SetMode(ctx, int(req.GameID), int32(req.Mode)); err != nil { return nil, status.Error(codes.Internal, "не удалось изменить режим экранов") } return &proto.SetScreenModeRsp{}, nil } func (s *server) SetScreenTimer(ctx context.Context, req *proto.SetScreenTimerReq) (*proto.SetScreenTimerRsp, error) { if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if req == nil || req.GameID <= 0 || req.DurationSeconds <= 0 || req.DurationSeconds > 24*60*60 || !screen_service.ValidTimerType(int32(req.TimerType)) { return nil, status.Error(codes.InvalidArgument, "некорректные параметры таймера") } label := strings.TrimSpace(req.Label) if label == "" { label = screen_service.TimerLabel(int32(req.TimerType)) } if len(label) > 200 { return nil, status.Error(codes.InvalidArgument, "слишком длинная надпись таймера") } if _, err := s.gameService.GetGame(ctx, int(req.GameID)); err != nil { return nil, status.Error(codes.NotFound, "игра не найдена") } if err := s.screenService.SetTimer(ctx, int(req.GameID), req.DurationSeconds, label, int32(req.TimerType)); err != nil { return nil, status.Error(codes.Internal, "не удалось настроить таймер экранов") } return &proto.SetScreenTimerRsp{}, nil } func (s *server) SendScreenCommand(ctx context.Context, req *proto.SendScreenCommandReq) (*proto.SendScreenCommandRsp, error) { if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") } if req == nil || req.GameID <= 0 || req.Command == proto.ScreenCommand_SCREEN_COMMAND_UNSPECIFIED { return nil, status.Error(codes.InvalidArgument, "некорректная команда экрана") } if _, err := s.gameService.GetGame(ctx, int(req.GameID)); err != nil { return nil, status.Error(codes.NotFound, "игра не найдена") } if isTimerCommand(req.Command) { if err := s.screenService.SetTimerCommand(ctx, int(req.GameID), int32(req.Command)); err != nil { return nil, status.Error(codes.Internal, "не удалось выполнить команду таймера") } return &proto.SendScreenCommandRsp{}, nil } if req.PositionMs < 0 { return nil, status.Error(codes.InvalidArgument, "некорректная позиция") } if req.Volume < 0 || req.Volume > 100 { return nil, status.Error(codes.InvalidArgument, "некорректная громкость") } if err := s.screenService.SetCommand(ctx, int(req.GameID), int32(req.Command), req.PositionMs, req.Volume); err != nil { return nil, status.Error(codes.Internal, "не удалось сохранить позицию экранов") } return &proto.SendScreenCommandRsp{}, nil } func isTimerCommand(command proto.ScreenCommand) bool { return command >= proto.ScreenCommand_SCREEN_COMMAND_TIMER_START && command <= proto.ScreenCommand_SCREEN_COMMAND_TIMER_RESET } func (s *server) GetScreenResults(ctx context.Context, req *proto.GetScreenResultsReq) (*proto.GetScreenResultsRsp, error) { if req == nil || req.ScreenID == "" || req.AccessToken == "" { return nil, status.Error(codes.InvalidArgument, "некорректные данные экрана") } id, err := strconv.Atoi(req.ScreenID) if err != nil || id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } session, err := s.screenService.Get(ctx, id, hashScreenToken(req.AccessToken)) if errors.Is(err, screen_service.ErrScreenNotFound) { return nil, status.Error(codes.Unauthenticated, "недействительная сессия экрана") } if err != nil || session.GameID == nil { return nil, status.Error(codes.FailedPrecondition, "экран не привязан к игре") } game, err := s.gameService.GetGame(ctx, *session.GameID) if err != nil { return nil, status.Error(codes.NotFound, "игра не найдена") } if game.Status != "finish" { return nil, status.Error(codes.FailedPrecondition, "результаты доступны после завершения игры") } results, err := s.answerService.GetGameResults(ctx, *session.GameID) if err != nil { return nil, status.Error(codes.Internal, "не удалось получить результаты") } return &proto.GetScreenResultsRsp{Questions: mapResultsQuestions(results.Questions), Teams: mapTeamResults(results.Teams)}, nil } func (s *server) GetScreenTeams(ctx context.Context, req *proto.GetScreenTeamsReq) (*proto.GetScreenTeamsRsp, error) { if req == nil || req.ScreenID == "" || req.AccessToken == "" { return nil, status.Error(codes.InvalidArgument, "некорректные данные экрана") } id, err := strconv.Atoi(req.ScreenID) if err != nil || id <= 0 { return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор экрана") } session, err := s.screenService.Get(ctx, id, hashScreenToken(req.AccessToken)) if errors.Is(err, screen_service.ErrScreenNotFound) { return nil, status.Error(codes.Unauthenticated, "недействительная сессия экрана") } if err != nil || session.GameID == nil { return nil, status.Error(codes.FailedPrecondition, "экран не привязан к игре") } teams, err := s.teamService.GetScreenTeams(ctx, *session.GameID) if err != nil { return nil, status.Error(codes.Internal, "не удалось получить команды") } result := make([]*proto.ScreenTeam, 0, len(teams)) for _, team := range teams { result = append(result, &proto.ScreenTeam{ID: team.ID, Name: team.Name}) } return &proto.GetScreenTeamsRsp{Teams: result}, nil }