From 0fb717f047c7ca3a628c758414ad589a27492583 Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Wed, 16 Sep 2026 18:56:50 +0700 Subject: [PATCH] add screens.go --- internal/app/screens.go | 270 ++++++++++++++++++++++++++++++++++++++++ internal/app/server.go | 255 ------------------------------------- 2 files changed, 270 insertions(+), 255 deletions(-) create mode 100644 internal/app/screens.go diff --git a/internal/app/screens.go b/internal/app/screens.go new file mode 100644 index 0000000..6eee94c --- /dev/null +++ b/internal/app/screens.go @@ -0,0 +1,270 @@ +package app + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "strconv" + "time" + + "evening_detective_server/internal/modules/processor_jwt" + "evening_detective_server/internal/modules/roles" + "evening_detective_server/internal/repos/screen_sessions_repo" + 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.screenRepo.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.screenRepo.Get(ctx, id, hashScreenToken(req.AccessToken)) + if errors.Is(err, screen_sessions_repo.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) 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.screenRepo.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 { + states = append(states, screenState(session)) + } + 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) + if err = s.screenRepo.Bind(ctx, id, int(req.GameId), hash); err != nil { + if errors.Is(err, screen_sessions_repo.ErrScreenNotFound) { + return nil, status.Error(codes.Unauthenticated, "недействительная сессия экрана") + } + return nil, status.Error(codes.Internal, "не удалось привязать экран") + } + session, err := s.screenRepo.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.screenRepo.Unbind(ctx, id); err != nil { + if errors.Is(err, screen_sessions_repo.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.screenRepo.SetSound(ctx, id, req.SoundEnabled); err != nil { + if errors.Is(err, screen_sessions_repo.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 := ctx.Value("claims").(*processor_jwt.JWTClaims) + 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_sessions_repo.ScreenSession) *proto.ScreenState { + 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, + } + 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_sessions_repo.ScreenSession, state *proto.ScreenState) { + if session.GameID == nil { + return + } + 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 || req.Mode == proto.ScreenMode_SCREEN_MODE_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 err := s.screenRepo.SetMode(ctx, int(req.GameId), int32(req.Mode)); err != nil { + return nil, status.Error(codes.Internal, "не удалось изменить режим экранов") + } + return &proto.SetScreenModeRsp{}, 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 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.screenRepo.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 (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.screenRepo.Get(ctx, id, hashScreenToken(req.AccessToken)) + if errors.Is(err, screen_sessions_repo.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.gameService.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 +} diff --git a/internal/app/server.go b/internal/app/server.go index e4d6729..fbf23b3 100644 --- a/internal/app/server.go +++ b/internal/app/server.go @@ -2,14 +2,10 @@ package app import ( "context" - "crypto/sha256" - "encoding/base64" "errors" "fmt" "net/netip" - "strconv" "strings" - "time" "evening_detective_server/docs" "evening_detective_server/internal/modules/file_storage" @@ -28,7 +24,6 @@ import ( "evening_detective_server/internal/services/users_service" proto "evening_detective_server/proto" - "github.com/google/uuid" "google.golang.org/genproto/googleapis/api/httpbody" "google.golang.org/grpc" "google.golang.org/grpc/codes" @@ -71,126 +66,6 @@ func NewServer( } } -func (s *server) CreateScreenSession(ctx context.Context, _ *proto.CreateScreenSessionReq) (*proto.CreateScreenSessionRsp, error) { - token := uuid.New().String() - id, err := s.screenRepo.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.screenRepo.Get(ctx, id, hashScreenToken(req.AccessToken)) - if errors.Is(err, screen_sessions_repo.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) 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.screenRepo.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 { - states = append(states, screenState(session)) - } - 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) - if err = s.screenRepo.Bind(ctx, id, int(req.GameId), hash); err != nil { - if errors.Is(err, screen_sessions_repo.ErrScreenNotFound) { - return nil, status.Error(codes.Unauthenticated, "недействительная сессия экрана") - } - return nil, status.Error(codes.Internal, "не удалось привязать экран") - } - session, err := s.screenRepo.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.screenRepo.Unbind(ctx, id); err != nil { - if errors.Is(err, screen_sessions_repo.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.screenRepo.SetSound(ctx, id, req.SoundEnabled); err != nil { - if errors.Is(err, screen_sessions_repo.ErrScreenNotFound) { - return nil, status.Error(codes.NotFound, "экран не найден") - } - return nil, status.Error(codes.Internal, "не удалось назначить экран со звуком") - } - return &proto.SetScreenSoundRsp{}, nil -} - func (s *server) GetGameMap(ctx context.Context, req *proto.GetGameMapReq) (*proto.GetGameMapRsp, error) { if !canManageScreens(ctx) { return nil, status.Error(codes.PermissionDenied, "permission denied") @@ -273,136 +148,6 @@ func (s *server) UpdateGameMapItem(ctx context.Context, req *proto.UpdateGameMap return &proto.UpdateGameMapItemRsp{}, nil } -func canManageScreens(ctx context.Context) bool { - claims, ok := ctx.Value("claims").(*processor_jwt.JWTClaims) - 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_sessions_repo.ScreenSession) *proto.ScreenState { - 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, - } - 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_sessions_repo.ScreenSession, state *proto.ScreenState) { - if session.GameID == nil { - return - } - 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 || req.Mode == proto.ScreenMode_SCREEN_MODE_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 err := s.screenRepo.SetMode(ctx, int(req.GameId), int32(req.Mode)); err != nil { - return nil, status.Error(codes.Internal, "не удалось изменить режим экранов") - } - return &proto.SetScreenModeRsp{}, 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 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.screenRepo.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 (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.screenRepo.Get(ctx, id, hashScreenToken(req.AccessToken)) - if errors.Is(err, screen_sessions_repo.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.gameService.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) Ping(_ context.Context, _ *proto.PingReq) (*proto.PingRsp, error) { return &proto.PingRsp{}, nil }