This commit is contained in:
2026-10-23 03:02:22 +07:00
parent 980ca18b60
commit 0879a8946a
20 changed files with 1827 additions and 960 deletions
+40 -7
View File
@@ -921,6 +921,10 @@ service EveningDetectiveServer {
}; };
} }
rpc GetScreenTeams(GetScreenTeamsReq) returns (GetScreenTeamsRsp) {
option (google.api.http) = { post: "/api/screens/teams" body: "*" };
}
rpc GetScreenResults(GetScreenResultsReq) returns (GetScreenResultsRsp) { rpc GetScreenResults(GetScreenResultsReq) returns (GetScreenResultsRsp) {
option (google.api.http) = { post: "/api/screens/results" body: "*" }; option (google.api.http) = { post: "/api/screens/results" body: "*" };
option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = {
@@ -1783,6 +1787,14 @@ enum ScreenMode {
SCREEN_MODE_RESULTS = 6; SCREEN_MODE_RESULTS = 6;
SCREEN_MODE_TIMER = 7; SCREEN_MODE_TIMER = 7;
SCREEN_MODE_TEXT = 8; SCREEN_MODE_TEXT = 8;
SCREEN_MODE_QR = 9;
SCREEN_MODE_TEAMS = 10;
}
enum ScreenTimerType {
SCREEN_TIMER_TYPE_BREAK = 0;
SCREEN_TIMER_TYPE_BEFORE_GAME = 1;
SCREEN_TIMER_TYPE_UNTIL_END = 2;
} }
enum ScreenTimerState { enum ScreenTimerState {
@@ -1830,6 +1842,8 @@ message ScreenState {
ScreenTimerState timer_state = 19; ScreenTimerState timer_state = 19;
string text = 20; string text = 20;
MusicState music = 21; MusicState music = 21;
string qr_url = 22;
ScreenTimerType timer_type = 23;
} }
message CreateScreenSessionReq {} message CreateScreenSessionReq {}
@@ -1966,15 +1980,18 @@ message UpdateGameMapItemRsp {}
message SetScreenModeReq { message SetScreenModeReq {
int32 gameID = 1; int32 gameID = 1;
ScreenMode mode = 2; ScreenMode mode = 2;
string qr_url = 3;
string text = 4;
} }
message SetScreenModeRsp { message SetScreenModeRsp {
} }
message SetScreenTimerReq { message SetScreenTimerReq {
int32 gameID = 1; int32 gameID = 1;
int64 duration_seconds = 2; int64 duration_seconds = 2;
string label = 3; string label = 3;
ScreenTimerType timer_type = 4;
} }
message SetScreenTimerRsp { message SetScreenTimerRsp {
@@ -1991,10 +2008,12 @@ message SendScreenCommandRsp {
} }
message ScreenPlanStep { message ScreenPlanStep {
ScreenMode mode = 1; ScreenMode mode = 1;
string text = 2; string text = 2;
string timer_label = 3; string timer_label = 3;
int64 timer_duration_seconds = 4; int64 timer_duration_seconds = 4;
string qr_url = 5;
ScreenTimerType timer_type = 6;
} }
message ScreenPlan { message ScreenPlan {
@@ -2029,6 +2048,20 @@ message SetScreenPlanStepRsp {
ScreenPlan plan = 1; ScreenPlan plan = 1;
} }
message ScreenTeam {
string ID = 1;
string name = 2;
}
message GetScreenTeamsReq {
string screenID = 1;
string access_token = 2;
}
message GetScreenTeamsRsp {
repeated ScreenTeam teams = 1;
}
message GetScreenResultsReq { message GetScreenResultsReq {
string screenID = 1; string screenID = 1;
string accessToken = 2; string accessToken = 2;
+1
View File
@@ -276,6 +276,7 @@ func main() {
"/crabs.evening_detective_server.EveningDetectiveServer/GetScreenState": true, "/crabs.evening_detective_server.EveningDetectiveServer/GetScreenState": true,
"/crabs.evening_detective_server.EveningDetectiveServer/AdvanceScreenMusic": true, "/crabs.evening_detective_server.EveningDetectiveServer/AdvanceScreenMusic": true,
"/crabs.evening_detective_server.EveningDetectiveServer/GetScreenResults": true, "/crabs.evening_detective_server.EveningDetectiveServer/GetScreenResults": true,
"/crabs.evening_detective_server.EveningDetectiveServer/GetScreenTeams": true,
"/crabs.evening_detective_server.EveningDetectiveServer/CreateGameReview": true, "/crabs.evening_detective_server.EveningDetectiveServer/CreateGameReview": true,
"/crabs.evening_detective_server.EveningDetectiveServer/GetGameReviews": true, "/crabs.evening_detective_server.EveningDetectiveServer/GetGameReviews": true,
}, },
+99 -1
View File
@@ -3055,6 +3055,38 @@
] ]
} }
}, },
"/api/screens/teams": {
"post": {
"operationId": "EveningDetectiveServer_GetScreenTeams",
"responses": {
"200": {
"description": "A successful response.",
"schema": {
"$ref": "#/definitions/evening_detective_serverGetScreenTeamsRsp"
}
},
"default": {
"description": "An unexpected error response.",
"schema": {
"$ref": "#/definitions/rpcStatus"
}
}
},
"parameters": [
{
"name": "body",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/evening_detective_serverGetScreenTeamsReq"
}
}
],
"tags": [
"EveningDetectiveServer"
]
}
},
"/api/screens/{screenID}/bind": { "/api/screens/{screenID}/bind": {
"post": { "post": {
"summary": "Привязать экран к игре", "summary": "Привязать экран к игре",
@@ -4387,6 +4419,12 @@
"properties": { "properties": {
"mode": { "mode": {
"$ref": "#/definitions/evening_detective_serverScreenMode" "$ref": "#/definitions/evening_detective_serverScreenMode"
},
"qrUrl": {
"type": "string"
},
"text": {
"type": "string"
} }
} }
}, },
@@ -4416,6 +4454,9 @@
}, },
"label": { "label": {
"type": "string" "type": "string"
},
"timerType": {
"$ref": "#/definitions/evening_detective_serverScreenTimerType"
} }
} }
}, },
@@ -5551,6 +5592,29 @@
} }
} }
}, },
"evening_detective_serverGetScreenTeamsReq": {
"type": "object",
"properties": {
"screenID": {
"type": "string"
},
"accessToken": {
"type": "string"
}
}
},
"evening_detective_serverGetScreenTeamsRsp": {
"type": "object",
"properties": {
"teams": {
"type": "array",
"items": {
"type": "object",
"$ref": "#/definitions/evening_detective_serverScreenTeam"
}
}
}
},
"evening_detective_serverGetTaskAssigneesRsp": { "evening_detective_serverGetTaskAssigneesRsp": {
"type": "object", "type": "object",
"properties": { "properties": {
@@ -6064,7 +6128,9 @@
"SCREEN_MODE_CONCLUSION", "SCREEN_MODE_CONCLUSION",
"SCREEN_MODE_RESULTS", "SCREEN_MODE_RESULTS",
"SCREEN_MODE_TIMER", "SCREEN_MODE_TIMER",
"SCREEN_MODE_TEXT" "SCREEN_MODE_TEXT",
"SCREEN_MODE_QR",
"SCREEN_MODE_TEAMS"
], ],
"default": "SCREEN_MODE_UNSPECIFIED" "default": "SCREEN_MODE_UNSPECIFIED"
}, },
@@ -6099,6 +6165,12 @@
"timerDurationSeconds": { "timerDurationSeconds": {
"type": "string", "type": "string",
"format": "int64" "format": "int64"
},
"qrUrl": {
"type": "string"
},
"timerType": {
"$ref": "#/definitions/evening_detective_serverScreenTimerType"
} }
} }
}, },
@@ -6173,6 +6245,23 @@
}, },
"music": { "music": {
"$ref": "#/definitions/evening_detective_serverMusicState" "$ref": "#/definitions/evening_detective_serverMusicState"
},
"qrUrl": {
"type": "string"
},
"timerType": {
"$ref": "#/definitions/evening_detective_serverScreenTimerType"
}
}
},
"evening_detective_serverScreenTeam": {
"type": "object",
"properties": {
"ID": {
"type": "string"
},
"name": {
"type": "string"
} }
} }
}, },
@@ -6187,6 +6276,15 @@
], ],
"default": "SCREEN_TIMER_STATE_UNSPECIFIED" "default": "SCREEN_TIMER_STATE_UNSPECIFIED"
}, },
"evening_detective_serverScreenTimerType": {
"type": "string",
"enum": [
"SCREEN_TIMER_TYPE_BREAK",
"SCREEN_TIMER_TYPE_BEFORE_GAME",
"SCREEN_TIMER_TYPE_UNTIL_END"
],
"default": "SCREEN_TIMER_TYPE_BREAK"
},
"evening_detective_serverSendScreenCommandRsp": { "evening_detective_serverSendScreenCommandRsp": {
"type": "object" "type": "object"
}, },
+4
View File
@@ -14,6 +14,8 @@ func mapScreenPlanSteps(steps []*proto.ScreenPlanStep) []screen_plan_service.Ste
} }
mapped = append(mapped, screen_plan_service.Step{ mapped = append(mapped, screen_plan_service.Step{
Mode: int32(step.Mode), Mode: int32(step.Mode),
QRURL: step.QrUrl,
TimerType: int32(step.TimerType),
Text: step.Text, Text: step.Text,
TimerLabel: step.TimerLabel, TimerLabel: step.TimerLabel,
TimerDurationSeconds: step.TimerDurationSeconds, TimerDurationSeconds: step.TimerDurationSeconds,
@@ -30,6 +32,8 @@ func mapScreenPlan(plan *screen_plan_service.Plan) *proto.ScreenPlan {
for _, step := range plan.Steps { for _, step := range plan.Steps {
steps = append(steps, &proto.ScreenPlanStep{ steps = append(steps, &proto.ScreenPlanStep{
Mode: proto.ScreenMode(step.Mode), Mode: proto.ScreenMode(step.Mode),
QrUrl: step.QRURL,
TimerType: proto.ScreenTimerType(step.TimerType),
Text: step.Text, Text: step.Text,
TimerLabel: step.TimerLabel, TimerLabel: step.TimerLabel,
TimerDurationSeconds: step.TimerDurationSeconds, TimerDurationSeconds: step.TimerDurationSeconds,
+40 -3
View File
@@ -222,6 +222,8 @@ func screenState(session *screen_service.ScreenSession) *proto.ScreenState {
TimerRemainingSeconds: timerRemaining, TimerRemainingSeconds: timerRemaining,
TimerState: timerState, TimerState: timerState,
Text: session.Text, Text: session.Text,
QrUrl: session.QRURL,
TimerType: proto.ScreenTimerType(session.TimerType),
} }
if time.Since(session.LastSeen) <= 5*time.Second { if time.Since(session.LastSeen) <= 5*time.Second {
state.Status = "connected" state.Status = "connected"
@@ -277,6 +279,15 @@ func (s *server) SetScreenMode(ctx context.Context, req *proto.SetScreenModeReq)
if _, err := s.gameService.GetGame(ctx, int(req.GameID)); err != nil { if _, err := s.gameService.GetGame(ctx, int(req.GameID)); err != nil {
return nil, status.Error(codes.NotFound, "игра не найдена") 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 { if err := s.screenService.SetMode(ctx, int(req.GameID), int32(req.Mode)); err != nil {
return nil, status.Error(codes.Internal, "не удалось изменить режим экранов") return nil, status.Error(codes.Internal, "не удалось изменить режим экранов")
} }
@@ -287,12 +298,12 @@ func (s *server) SetScreenTimer(ctx context.Context, req *proto.SetScreenTimerRe
if !canManageScreens(ctx) { if !canManageScreens(ctx) {
return nil, status.Error(codes.PermissionDenied, "permission denied") return nil, status.Error(codes.PermissionDenied, "permission denied")
} }
if req == nil || req.GameID <= 0 || req.DurationSeconds <= 0 || req.DurationSeconds > 24*60*60 { 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, "некорректные параметры таймера") return nil, status.Error(codes.InvalidArgument, "некорректные параметры таймера")
} }
label := strings.TrimSpace(req.Label) label := strings.TrimSpace(req.Label)
if label == "" { if label == "" {
label = "Перерыв" label = screen_service.TimerLabel(int32(req.TimerType))
} }
if len(label) > 200 { if len(label) > 200 {
return nil, status.Error(codes.InvalidArgument, "слишком длинная надпись таймера") return nil, status.Error(codes.InvalidArgument, "слишком длинная надпись таймера")
@@ -300,7 +311,7 @@ func (s *server) SetScreenTimer(ctx context.Context, req *proto.SetScreenTimerRe
if _, err := s.gameService.GetGame(ctx, int(req.GameID)); err != nil { if _, err := s.gameService.GetGame(ctx, int(req.GameID)); err != nil {
return nil, status.Error(codes.NotFound, "игра не найдена") return nil, status.Error(codes.NotFound, "игра не найдена")
} }
if err := s.screenService.SetTimer(ctx, int(req.GameID), req.DurationSeconds, label); err != nil { if err := s.screenService.SetTimer(ctx, int(req.GameID), req.DurationSeconds, label, int32(req.TimerType)); err != nil {
return nil, status.Error(codes.Internal, "не удалось настроить таймер экранов") return nil, status.Error(codes.Internal, "не удалось настроить таймер экранов")
} }
return &proto.SetScreenTimerRsp{}, nil return &proto.SetScreenTimerRsp{}, nil
@@ -366,3 +377,29 @@ func (s *server) GetScreenResults(ctx context.Context, req *proto.GetScreenResul
} }
return &proto.GetScreenResultsRsp{Questions: mapResultsQuestions(results.Questions), Teams: mapTeamResults(results.Teams)}, nil 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
}
+19
View File
@@ -26,3 +26,22 @@ func TestSetScreenModeRejectsRemovedModes(t *testing.T) {
} }
} }
} }
func TestScreenTeamsRejectsInvalidAccess(t *testing.T) {
tests := []struct {
name string
req *proto.GetScreenTeamsReq
}{
{"missing", nil},
{"missing token", &proto.GetScreenTeamsReq{ScreenID: "1"}},
{"bad ID", &proto.GetScreenTeamsReq{ScreenID: "bad", AccessToken: "token"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := (&server{}).GetScreenTeams(context.Background(), tt.req)
if status.Code(err) != codes.InvalidArgument {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
@@ -14,6 +14,8 @@ var ErrPlanNotFound = errors.New("план экранов не найден")
// Step is a stored screen plan step. // Step is a stored screen plan step.
type Step struct { type Step struct {
QRURL string `json:"qrUrl,omitempty"`
TimerType int32 `json:"timerType,omitempty"`
Mode int32 `json:"mode"` Mode int32 `json:"mode"`
Text string `json:"text,omitempty"` Text string `json:"text,omitempty"`
TimerLabel string `json:"timerLabel,omitempty"` TimerLabel string `json:"timerLabel,omitempty"`
+62 -1
View File
@@ -8,6 +8,8 @@ import (
"sync" "sync"
"testing" "testing"
"time" "time"
"evening_detective_server/internal/services/screen_service"
) )
func TestReorderPreservesPlaybackAndRejectsChangedMembership(t *testing.T) { func TestReorderPreservesPlaybackAndRejectsChangedMembership(t *testing.T) {
@@ -135,7 +137,7 @@ func TestBindInitializesAudioWithoutChangingTransport(t *testing.T) {
if isNewBinding { if isNewBinding {
t.Fatal("repeat binding would reset transport") t.Fatal("repeat binding would reset transport")
} }
requireNoError(t, f.screens.InitializeScreen(ctx, second, f.gameID, 1, "", "Pause", 600)) requireNoError(t, f.screens.InitializeScreen(ctx, second, f.gameID, 1, "", "Pause", 600, "", 0))
original, err := f.screens.Get(ctx, first, "first") original, err := f.screens.Get(ctx, first, "first")
requireNoError(t, err) requireNoError(t, err)
joined, err := f.screens.Get(ctx, second, "second") joined, err := f.screens.Get(ctx, second, "second")
@@ -245,3 +247,62 @@ func TestConcurrentBindingsAssignSoundOnce(t *testing.T) {
}) })
} }
} }
func TestNewScreenStatePersistsAndCopiesToJoiningScreen(t *testing.T) {
tests := []struct {
name string
mode int32
link, label string
timerType int32
}{
{name: "QR", mode: 9, link: "https://example.org/join"},
{name: "timer", mode: 7, label: "Обсуждение", timerType: 2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f := newPlaylistFixture(t)
ctx := context.Background()
first, err := f.screens.Create(ctx, "first-hash")
requireNoError(t, err)
requireNoError(t, f.screens.Bind(ctx, first, f.gameID, "first-hash"))
if tt.mode == 9 {
service := screen_service.NewScreenService(f.screens)
requireNoError(t, service.InitializeScreen(ctx, first, f.gameID, 9, "Присоединяйтесь", "", 0, tt.link, 0))
initialized, err := f.screens.Get(ctx, first, "first-hash")
requireNoError(t, err)
if initialized.TimerDurationSeconds != 600 {
t.Fatal("non-timer plan initialized an invalid timer")
}
requireNoError(t, f.screens.SetQR(ctx, f.gameID, tt.link, "Присоединяйтесь"))
} else {
requireNoError(t, f.screens.SetTimer(ctx, f.gameID, 60, tt.label, tt.timerType))
}
second, err := f.screens.Create(ctx, "second-hash")
requireNoError(t, err)
requireNoError(t, f.screens.Bind(ctx, second, f.gameID, "second-hash"))
requireNoError(t, f.screens.InitializeScreen(ctx, second, f.gameID, 1, "", "Перерыв", 600, "", 0))
copied, err := f.screens.Get(ctx, second, "second-hash")
requireNoError(t, err)
if copied.Mode != tt.mode || copied.QRURL != tt.link || copied.TimerType != tt.timerType {
t.Fatalf("joining screen lost state: %+v", copied)
}
if tt.mode == 9 && copied.Text != "Присоединяйтесь" {
t.Fatal("lost QR caption")
}
if tt.mode == 7 && (copied.TimerLabel != tt.label || copied.TimerDurationSeconds != 60) {
t.Fatal("lost timer settings")
}
all, err := f.screens.ListByGame(ctx, f.gameID)
requireNoError(t, err)
if len(all) != 2 || all[1].QRURL != copied.QRURL || all[1].TimerType != copied.TimerType {
t.Fatal("state list lost new fields")
}
requireNoError(t, f.screens.Unbind(ctx, second))
detached, err := f.screens.Get(ctx, second, "second-hash")
requireNoError(t, err)
if detached.QRURL != "" || detached.TimerType != 0 {
t.Fatal("unbind retained another game's settings")
}
})
}
}
+99 -20
View File
@@ -29,6 +29,8 @@ type ScreenSession struct {
TimerRemainingSeconds int64 TimerRemainingSeconds int64
TimerState int32 TimerState int32
TimerEndsAt *time.Time TimerEndsAt *time.Time
QRURL string
TimerType int32
Text string Text string
} }
@@ -55,20 +57,41 @@ func (r *ScreenSessionsRepo) Create(ctx context.Context, tokenHash string) (int,
// Get returns a screen session by ID and token hash. // Get returns a screen session by ID and token hash.
func (r *ScreenSessionsRepo) Get(ctx context.Context, id int, tokenHash string) (*ScreenSession, error) { func (r *ScreenSessionsRepo) Get(ctx context.Context, id int, tokenHash string) (*ScreenSession, error) {
session := &ScreenSession{ID: id} session := &ScreenSession{ID: id}
err := r.pool.QueryRow(ctx, ` err := r.pool.QueryRow(
SELECT game_id, sound_enabled, last_seen_at, mode, state_version, position_ms, command, volume, ctx,
timer_label, timer_duration_seconds, timer_remaining_seconds, timer_state, timer_ends_at, text `SELECT
game_id,
sound_enabled,
last_seen_at,
mode,
state_version,
position_ms,
command,
volume,
timer_label,
timer_duration_seconds,
timer_remaining_seconds,
timer_state,
timer_ends_at,
text,
qr_url,
timer_type
FROM screen_sessions FROM screen_sessions
WHERE id = $1 AND access_token_hash = $2`, id, tokenHash). WHERE id = $1 AND access_token_hash = $2`,
id,
tokenHash,
).
Scan(&session.GameID, &session.Sound, &session.LastSeen, &session.Mode, &session.Version, &session.Position, &session.Command, &session.Volume, Scan(&session.GameID, &session.Sound, &session.LastSeen, &session.Mode, &session.Version, &session.Position, &session.Command, &session.Volume,
&session.TimerLabel, &session.TimerDurationSeconds, &session.TimerRemainingSeconds, &session.TimerState, &session.TimerEndsAt, &session.Text) &session.TimerLabel, &session.TimerDurationSeconds, &session.TimerRemainingSeconds, &session.TimerState, &session.TimerEndsAt, &session.Text, &session.QRURL, &session.TimerType)
if errors.Is(err, pgx.ErrNoRows) { if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrScreenNotFound return nil, ErrScreenNotFound
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
_, err = r.pool.Exec(ctx, `UPDATE screen_sessions SET last_seen_at = NOW() WHERE id = $1`, id) _, err = r.pool.Exec(ctx, `UPDATE screen_sessions SET last_seen_at = NOW() WHERE id = $1`,
id,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -78,12 +101,31 @@ func (r *ScreenSessionsRepo) Get(ctx context.Context, id int, tokenHash string)
// ListByGame returns screen sessions for a game. // ListByGame returns screen sessions for a game.
func (r *ScreenSessionsRepo) ListByGame(ctx context.Context, gameID int) ([]*ScreenSession, error) { func (r *ScreenSessionsRepo) ListByGame(ctx context.Context, gameID int) ([]*ScreenSession, error) {
rows, err := r.pool.Query(ctx, ` rows, err := r.pool.Query(
SELECT id, game_id, sound_enabled, last_seen_at, mode, state_version, position_ms, command, volume, ctx,
timer_label, timer_duration_seconds, timer_remaining_seconds, timer_state, timer_ends_at, text `SELECT
id,
game_id,
sound_enabled,
last_seen_at,
mode,
state_version,
position_ms,
command,
volume,
timer_label,
timer_duration_seconds,
timer_remaining_seconds,
timer_state,
timer_ends_at,
text,
qr_url,
timer_type
FROM screen_sessions FROM screen_sessions
WHERE game_id = $1 WHERE game_id = $1
ORDER BY id`, gameID) ORDER BY id`,
gameID,
)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -93,7 +135,7 @@ func (r *ScreenSessionsRepo) ListByGame(ctx context.Context, gameID int) ([]*Scr
for rows.Next() { for rows.Next() {
session := &ScreenSession{} session := &ScreenSession{}
if err := rows.Scan(&session.ID, &session.GameID, &session.Sound, &session.LastSeen, &session.Mode, &session.Version, &session.Position, &session.Command, &session.Volume, if err := rows.Scan(&session.ID, &session.GameID, &session.Sound, &session.LastSeen, &session.Mode, &session.Version, &session.Position, &session.Command, &session.Volume,
&session.TimerLabel, &session.TimerDurationSeconds, &session.TimerRemainingSeconds, &session.TimerState, &session.TimerEndsAt, &session.Text); err != nil { &session.TimerLabel, &session.TimerDurationSeconds, &session.TimerRemainingSeconds, &session.TimerState, &session.TimerEndsAt, &session.Text, &session.QRURL, &session.TimerType); err != nil {
return nil, err return nil, err
} }
sessions = append(sessions, session) sessions = append(sessions, session)
@@ -148,10 +190,12 @@ func (r *ScreenSessionsRepo) SetCommand(ctx context.Context, gameID int, command
} }
// SetTimer updates the display timer for a game. // SetTimer updates the display timer for a game.
func (r *ScreenSessionsRepo) SetTimer(ctx context.Context, gameID int, duration int64, label string) error { func (r *ScreenSessionsRepo) SetTimer(ctx context.Context, gameID int, duration int64, label string, timerType int32) error {
_, err := r.pool.Exec(ctx, ` _, err := r.pool.Exec(
UPDATE screen_sessions ctx,
`UPDATE screen_sessions
SET mode = 7, SET mode = 7,
timer_type = $4,
timer_label = $2, timer_label = $2,
timer_duration_seconds = $3, timer_duration_seconds = $3,
timer_remaining_seconds = $3, timer_remaining_seconds = $3,
@@ -161,7 +205,12 @@ func (r *ScreenSessionsRepo) SetTimer(ctx context.Context, gameID int, duration
state_version = state_version + 1, state_version = state_version + 1,
text = '', text = '',
updated_at = NOW() updated_at = NOW()
WHERE game_id = $1`, gameID, label, duration) WHERE game_id = $1`,
gameID,
label,
duration,
timerType,
)
return err return err
} }
@@ -280,12 +329,15 @@ func (r *ScreenSessionsRepo) BindWithAudio(ctx context.Context, id, gameID int,
// Unbind detaches a screen session from its game. // Unbind detaches a screen session from its game.
func (r *ScreenSessionsRepo) Unbind(ctx context.Context, id int) error { func (r *ScreenSessionsRepo) Unbind(ctx context.Context, id int) error {
result, err := r.pool.Exec(ctx, ` result, err := r.pool.Exec(
UPDATE screen_sessions ctx,
`UPDATE screen_sessions
SET game_id = NULL, SET game_id = NULL,
mode = 1, mode = 1,
command = 0, command = 0,
position_ms = 0, position_ms = 0,
qr_url = '',
timer_type = 0,
timer_label = 'Перерыв', timer_label = 'Перерыв',
timer_duration_seconds = 600, timer_duration_seconds = 600,
timer_remaining_seconds = 600, timer_remaining_seconds = 600,
@@ -294,7 +346,9 @@ func (r *ScreenSessionsRepo) Unbind(ctx context.Context, id int) error {
text = '', text = '',
state_version = state_version + 1, state_version = state_version + 1,
updated_at = NOW() updated_at = NOW()
WHERE id = $1`, id) WHERE id = $1`,
id,
)
if err != nil { if err != nil {
return err return err
} }
@@ -339,12 +393,14 @@ func (r *ScreenSessionsRepo) SetSound(ctx context.Context, id int, enabled bool)
} }
// InitializeScreen copies a live display or uses the plan for the first screen. // InitializeScreen copies a live display or uses the plan for the first screen.
func (r *ScreenSessionsRepo) InitializeScreen(ctx context.Context, id, gameID int, mode int32, text, label string, duration int64) error { func (r *ScreenSessionsRepo) InitializeScreen(ctx context.Context, id, gameID int, mode int32, text, label string, duration int64, qrURL string, timerType int32) error {
_, err := r.pool.Exec( _, err := r.pool.Exec(
ctx, ctx,
`UPDATE screen_sessions target `UPDATE screen_sessions target
SET mode = COALESCE(source.mode, $3), SET mode = COALESCE(source.mode, $3),
text = COALESCE(source.text, $4), text = COALESCE(source.text, $4),
qr_url = COALESCE(source.qr_url, $7),
timer_type = COALESCE(source.timer_type, $8),
command = COALESCE(source.command, 0), command = COALESCE(source.command, 0),
position_ms = COALESCE(source.position_ms, 0), position_ms = COALESCE(source.position_ms, 0),
timer_label = COALESCE(source.timer_label, $5), timer_label = COALESCE(source.timer_label, $5),
@@ -365,7 +421,9 @@ func (r *ScreenSessionsRepo) InitializeScreen(ctx context.Context, id, gameID in
timer_duration_seconds, timer_duration_seconds,
timer_remaining_seconds, timer_remaining_seconds,
timer_state, timer_state,
timer_ends_at timer_ends_at,
qr_url,
timer_type
FROM screen_sessions FROM screen_sessions
WHERE game_id = $2 AND id <> $1 WHERE game_id = $2 AND id <> $1
ORDER BY updated_at DESC, id DESC ORDER BY updated_at DESC, id DESC
@@ -378,6 +436,27 @@ func (r *ScreenSessionsRepo) InitializeScreen(ctx context.Context, id, gameID in
text, text,
label, label,
duration, duration,
qrURL,
timerType,
)
return err
}
// SetQR updates QR content and mode together.
func (r *ScreenSessionsRepo) SetQR(ctx context.Context, gameID int, link, text string) error {
_, err := r.pool.Exec(
ctx,
`UPDATE screen_sessions
SET mode = 9,
qr_url = $2,
text = $3,
command = 0,
state_version = state_version + 1,
updated_at = NOW()
WHERE game_id = $1`,
gameID,
link,
text,
) )
return err return err
} }
@@ -7,6 +7,8 @@ func mapPlan(plan *game_screen_plans_repo.Plan) *Plan {
for _, step := range plan.Steps { for _, step := range plan.Steps {
steps = append(steps, Step{ steps = append(steps, Step{
Mode: step.Mode, Mode: step.Mode,
QRURL: step.QRURL,
TimerType: step.TimerType,
Text: step.Text, Text: step.Text,
TimerLabel: step.TimerLabel, TimerLabel: step.TimerLabel,
TimerDurationSeconds: step.TimerDurationSeconds, TimerDurationSeconds: step.TimerDurationSeconds,
@@ -20,6 +22,8 @@ func mapSteps(steps []Step) []game_screen_plans_repo.Step {
for _, step := range steps { for _, step := range steps {
mapped = append(mapped, game_screen_plans_repo.Step{ mapped = append(mapped, game_screen_plans_repo.Step{
Mode: step.Mode, Mode: step.Mode,
QRURL: step.QRURL,
TimerType: step.TimerType,
Text: step.Text, Text: step.Text,
TimerLabel: step.TimerLabel, TimerLabel: step.TimerLabel,
TimerDurationSeconds: step.TimerDurationSeconds, TimerDurationSeconds: step.TimerDurationSeconds,
@@ -21,6 +21,8 @@ var (
// Step is a screen plan step used by the application layer. // Step is a screen plan step used by the application layer.
type Step struct { type Step struct {
QRURL string
TimerType int32
Mode int32 Mode int32
Text string Text string
TimerLabel string TimerLabel string
@@ -103,13 +105,13 @@ func (s *ScreenPlanService) SetStep(ctx context.Context, gameID, currentStep int
func (s *ScreenPlanService) ApplyCurrent(ctx context.Context, gameID, screenID int) error { func (s *ScreenPlanService) ApplyCurrent(ctx context.Context, gameID, screenID int) error {
plan, err := s.Get(ctx, gameID) plan, err := s.Get(ctx, gameID)
if errors.Is(err, ErrPlanNotFound) { if errors.Is(err, ErrPlanNotFound) {
return s.screenService.InitializeScreen(ctx, screenID, gameID, 1, "", "Перерыв", 600) return s.screenService.InitializeScreen(ctx, screenID, gameID, 1, "", "Перерыв", 600, "", 0)
} }
if err != nil { if err != nil {
return err return err
} }
step := plan.Steps[plan.CurrentStep] step := plan.Steps[plan.CurrentStep]
return s.screenService.InitializeScreen(ctx, screenID, gameID, step.Mode, step.Text, step.TimerLabel, step.TimerDurationSeconds) return s.screenService.InitializeScreen(ctx, screenID, gameID, step.Mode, step.Text, step.TimerLabel, step.TimerDurationSeconds, step.QRURL, step.TimerType)
} }
func normalizePlan(steps []Step, currentStep int) ([]Step, error) { func normalizePlan(steps []Step, currentStep int) ([]Step, error) {
@@ -126,12 +128,18 @@ func normalizePlan(steps []Step, currentStep int) ([]Step, error) {
if step.Mode == 8 && (step.Text == "" || len(step.Text) > 10000) { if step.Mode == 8 && (step.Text == "" || len(step.Text) > 10000) {
return nil, ErrInvalidPlan return nil, ErrInvalidPlan
} }
if step.Mode == 9 && !screen_service.ValidQR(step.QRURL, step.Text) {
return nil, ErrInvalidPlan
}
if !screen_service.ValidTimerType(step.TimerType) {
return nil, ErrInvalidPlan
}
if step.Mode == 7 { if step.Mode == 7 {
if step.TimerDurationSeconds <= 0 || step.TimerDurationSeconds > 24*60*60 { if step.TimerDurationSeconds <= 0 || step.TimerDurationSeconds > 24*60*60 {
return nil, ErrInvalidPlan return nil, ErrInvalidPlan
} }
if step.TimerLabel == "" { if step.TimerLabel == "" {
step.TimerLabel = "Перерыв" step.TimerLabel = screen_service.TimerLabel(step.TimerType)
} }
if len(step.TimerLabel) > 200 { if len(step.TimerLabel) > 200 {
return nil, ErrInvalidPlan return nil, ErrInvalidPlan
@@ -144,11 +152,14 @@ func normalizePlan(steps []Step, currentStep int) ([]Step, error) {
func (s *ScreenPlanService) applyStep(ctx context.Context, gameID int, plan *Plan) error { func (s *ScreenPlanService) applyStep(ctx context.Context, gameID int, plan *Plan) error {
step := plan.Steps[plan.CurrentStep] step := plan.Steps[plan.CurrentStep]
if step.Mode == 9 {
return s.screenService.SetQR(ctx, gameID, step.QRURL, step.Text)
}
if step.Mode == 8 { if step.Mode == 8 {
return s.screenService.SetText(ctx, gameID, step.Text) return s.screenService.SetText(ctx, gameID, step.Text)
} }
if step.Mode == 7 { if step.Mode == 7 {
return s.screenService.SetTimer(ctx, gameID, step.TimerDurationSeconds, step.TimerLabel) return s.screenService.SetTimer(ctx, gameID, step.TimerDurationSeconds, step.TimerLabel, step.TimerType)
} }
return s.screenService.SetMode(ctx, gameID, step.Mode) return s.screenService.SetMode(ctx, gameID, step.Mode)
} }
@@ -3,6 +3,8 @@ package screen_plan_service
import ( import (
"errors" "errors"
"testing" "testing"
"evening_detective_server/internal/repos/game_screen_plans_repo"
) )
func TestNormalizePlan(t *testing.T) { func TestNormalizePlan(t *testing.T) {
@@ -52,3 +54,38 @@ func TestNormalizePlan(t *testing.T) {
}) })
} }
} }
func TestNormalizeNewScreens(t *testing.T) {
tests := []struct {
name string
step Step
wantLabel string
wantErr bool
}{
{name: "QR", step: Step{Mode: 9, QRURL: "https://example.org", Text: ""}},
{name: "teams", step: Step{Mode: 10}},
{name: "invalid QR", step: Step{Mode: 9, QRURL: "javascript:alert(1)"}, wantErr: true},
{name: "before game", step: Step{Mode: 7, TimerType: 1, TimerDurationSeconds: 60}, wantLabel: "До начала игры"},
{name: "until end", step: Step{Mode: 7, TimerType: 2, TimerDurationSeconds: 60}, wantLabel: "До конца игры"},
{name: "custom legacy", step: Step{Mode: 7, TimerLabel: "Обсуждение", TimerDurationSeconds: 60}, wantLabel: "Обсуждение"},
{name: "invalid type", step: Step{Mode: 7, TimerType: 3, TimerDurationSeconds: 60}, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := normalizePlan([]Step{tt.step}, 0)
if (err != nil) != tt.wantErr {
t.Fatalf("normalizePlan() error: %v", err)
}
if tt.wantErr {
return
}
if result[0].TimerLabel != tt.wantLabel {
t.Fatalf("label = %q", result[0].TimerLabel)
}
roundTrip := mapPlan(&game_screen_plans_repo.Plan{Steps: mapSteps(result)})
if roundTrip.Steps[0] != result[0] {
t.Fatal("lost step fields")
}
})
}
}
@@ -23,6 +23,8 @@ type ScreenSession struct {
TimerRemainingSeconds int64 TimerRemainingSeconds int64
TimerState int32 TimerState int32
TimerEndsAt *time.Time TimerEndsAt *time.Time
QRURL string
TimerType int32
Text string Text string
} }
@@ -43,5 +45,7 @@ func mapSession(session *screen_sessions_repo.ScreenSession) *ScreenSession {
TimerState: session.TimerState, TimerState: session.TimerState,
TimerEndsAt: session.TimerEndsAt, TimerEndsAt: session.TimerEndsAt,
Text: session.Text, Text: session.Text,
QRURL: session.QRURL,
TimerType: session.TimerType,
} }
} }
+46 -5
View File
@@ -3,6 +3,8 @@ package screen_service
import ( import (
"context" "context"
"errors" "errors"
"net/url"
"strings"
"evening_detective_server/internal/repos/screen_sessions_repo" "evening_detective_server/internal/repos/screen_sessions_repo"
) )
@@ -53,7 +55,7 @@ func (s *ScreenService) ListByGame(ctx context.Context, gameID int) ([]*ScreenSe
// ValidMode reports whether mode is a supported display mode. // ValidMode reports whether mode is a supported display mode.
func ValidMode(mode int32) bool { func ValidMode(mode int32) bool {
switch mode { switch mode {
case 1, 2, 4, 6, 7, 8: case 1, 2, 4, 6, 7, 8, 9, 10:
return true return true
default: default:
return false return false
@@ -69,8 +71,11 @@ func (s *ScreenService) SetMode(ctx context.Context, gameID int, mode int32) err
} }
// SetTimer updates the display timer for a game. // SetTimer updates the display timer for a game.
func (s *ScreenService) SetTimer(ctx context.Context, gameID int, duration int64, label string) error { func (s *ScreenService) SetTimer(ctx context.Context, gameID int, duration int64, label string, timerType int32) error {
return s.repo.SetTimer(ctx, gameID, duration, label) if !ValidTimerType(timerType) {
return ErrInvalidMode
}
return s.repo.SetTimer(ctx, gameID, duration, label, timerType)
} }
// SetText updates the text screen for a game. // SetText updates the text screen for a game.
@@ -112,6 +117,42 @@ func mapError(err error) error {
} }
// InitializeScreen joins the current display without resetting other screens. // InitializeScreen joins the current display without resetting other screens.
func (s *ScreenService) InitializeScreen(ctx context.Context, id, gameID int, mode int32, text, label string, duration int64) error { func (s *ScreenService) InitializeScreen(ctx context.Context, id, gameID int, mode int32, text, label string, duration int64, qrURL string, timerType int32) error {
return s.repo.InitializeScreen(ctx, id, gameID, mode, text, label, duration) // Non-timer steps have no duration, but every state includes a valid timer.
if mode != 7 && duration == 0 {
duration = 600
}
if label == "" {
label = TimerLabel(timerType)
}
return s.repo.InitializeScreen(ctx, id, gameID, mode, text, label, duration, qrURL, timerType)
}
// ValidQR validates a URL and caption without fetching the URL.
func ValidQR(link, text string) bool {
u, err := url.Parse(link)
return err == nil && (u.Scheme == "https" || u.Scheme == "http") && u.Hostname() != "" && u.User == nil && len(link) <= 2048 && len(text) <= 10000 && strings.TrimSpace(link) == link
}
// ValidTimerType reports whether the timer category is supported.
func ValidTimerType(kind int32) bool { return kind >= 0 && kind <= 2 }
// TimerLabel returns the initial caption for a timer category.
func TimerLabel(kind int32) string {
switch kind {
case 1:
return "До начала игры"
case 2:
return "До конца игры"
default:
return "Перерыв"
}
}
// SetQR updates the QR screen for a game.
func (s *ScreenService) SetQR(ctx context.Context, gameID int, link, text string) error {
if !ValidQR(link, text) {
return ErrInvalidMode
}
return s.repo.SetQR(ctx, gameID, link, text)
} }
@@ -3,14 +3,38 @@ package screen_service
import ( import (
"context" "context"
"errors" "errors"
"strings"
"testing" "testing"
) )
func TestSetModeRejectsUnsupportedModes(t *testing.T) { func TestSetModeRejectsUnsupportedModes(t *testing.T) {
s := NewScreenService(nil) s := NewScreenService(nil)
for _, mode := range []int32{-1, 0, 3, 5, 9} { for _, mode := range []int32{-1, 0, 3, 5, 11} {
if err := s.SetMode(context.Background(), 1, mode); !errors.Is(err, ErrInvalidMode) { if err := s.SetMode(context.Background(), 1, mode); !errors.Is(err, ErrInvalidMode) {
t.Errorf("SetMode(%d) = %v, want ErrInvalidMode", mode, err) t.Errorf("SetMode(%d) = %v, want ErrInvalidMode", mode, err)
} }
} }
} }
func TestValidQR(t *testing.T) {
tests := []struct {
name, link, caption string
valid bool
}{
{"link without caption", "https://example.org/join?q=1#join", "", true},
{"caption", "http://localhost:8080", "Команды", true},
{"missing link", "", "Текст", false},
{"script", "javascript:alert(1)", "", false},
{"missing host", "https:///path", "", false},
{"credentials", "https://user:secret@example.org", "", false},
{"long link", "https://example.org/" + strings.Repeat("x", 2048), "", false},
{"long caption", "https://example.org", strings.Repeat("я", 5001), false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ValidQR(tt.link, tt.caption); got != tt.valid {
t.Fatalf("ValidQR() = %v", got)
}
})
}
}
@@ -210,3 +210,8 @@ func (s *TeamService) CheckPresence(ctx context.Context, teamID string, password
} }
return team.GameID, nil return team.GameID, nil
} }
// GetScreenTeams returns active registrations for the public screen.
func (s *TeamService) GetScreenTeams(ctx context.Context, gameID int) ([]*repos.Team, error) {
return s.teamsRepo.GetActiveTeamsByGameID(ctx, gameID)
}
@@ -0,0 +1,7 @@
-- +goose Up
ALTER TABLE screen_sessions
ADD COLUMN qr_url TEXT NOT NULL DEFAULT '',
ADD COLUMN timer_type INTEGER NOT NULL DEFAULT 0 CHECK (timer_type BETWEEN 0 AND 2);
-- +goose Down
ALTER TABLE screen_sessions DROP COLUMN qr_url, DROP COLUMN timer_type;
+1214 -918
View File
File diff suppressed because it is too large Load Diff
+66
View File
@@ -3248,6 +3248,33 @@ func local_request_EveningDetectiveServer_SetScreenPlanStep_0(ctx context.Contex
return msg, metadata, err return msg, metadata, err
} }
func request_EveningDetectiveServer_GetScreenTeams_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetScreenTeamsReq
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
if req.Body != nil {
_, _ = io.Copy(io.Discard, req.Body)
}
msg, err := client.GetScreenTeams(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
return msg, metadata, err
}
func local_request_EveningDetectiveServer_GetScreenTeams_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var (
protoReq GetScreenTeamsReq
metadata runtime.ServerMetadata
)
if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) {
return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
}
msg, err := server.GetScreenTeams(ctx, &protoReq)
return msg, metadata, err
}
func request_EveningDetectiveServer_GetScreenResults_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { func request_EveningDetectiveServer_GetScreenResults_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
var ( var (
protoReq GetScreenResultsReq protoReq GetScreenResultsReq
@@ -6262,6 +6289,26 @@ func RegisterEveningDetectiveServerHandlerServer(ctx context.Context, mux *runti
} }
forward_EveningDetectiveServer_SetScreenPlanStep_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) forward_EveningDetectiveServer_SetScreenPlanStep_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
}) })
mux.Handle(http.MethodPost, pattern_EveningDetectiveServer_GetScreenTeams_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
var stream runtime.ServerTransportStream
ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/GetScreenTeams", runtime.WithHTTPPathPattern("/api/screens/teams"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := local_request_EveningDetectiveServer_GetScreenTeams_0(annotatedContext, inboundMarshaler, server, req, pathParams)
md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_GetScreenTeams_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_EveningDetectiveServer_GetScreenResults_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle(http.MethodPost, pattern_EveningDetectiveServer_GetScreenResults_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
@@ -8364,6 +8411,23 @@ func RegisterEveningDetectiveServerHandlerClient(ctx context.Context, mux *runti
} }
forward_EveningDetectiveServer_SetScreenPlanStep_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) forward_EveningDetectiveServer_SetScreenPlanStep_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
}) })
mux.Handle(http.MethodPost, pattern_EveningDetectiveServer_GetScreenTeams_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context())
defer cancel()
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/crabs.evening_detective_server.EveningDetectiveServer/GetScreenTeams", runtime.WithHTTPPathPattern("/api/screens/teams"))
if err != nil {
runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
return
}
resp, md, err := request_EveningDetectiveServer_GetScreenTeams_0(annotatedContext, inboundMarshaler, client, req, pathParams)
annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md)
if err != nil {
runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err)
return
}
forward_EveningDetectiveServer_GetScreenTeams_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
mux.Handle(http.MethodPost, pattern_EveningDetectiveServer_GetScreenResults_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { mux.Handle(http.MethodPost, pattern_EveningDetectiveServer_GetScreenResults_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
ctx, cancel := context.WithCancel(req.Context()) ctx, cancel := context.WithCancel(req.Context())
defer cancel() defer cancel()
@@ -8964,6 +9028,7 @@ var (
pattern_EveningDetectiveServer_GetScreenPlan_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "gameID", "screen-plan"}, "")) pattern_EveningDetectiveServer_GetScreenPlan_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "gameID", "screen-plan"}, ""))
pattern_EveningDetectiveServer_SaveScreenPlan_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "gameID", "screen-plan"}, "")) pattern_EveningDetectiveServer_SaveScreenPlan_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "gameID", "screen-plan"}, ""))
pattern_EveningDetectiveServer_SetScreenPlanStep_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 2, 4}, []string{"api", "games", "gameID", "screen-plan", "step"}, "")) pattern_EveningDetectiveServer_SetScreenPlanStep_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 2, 4}, []string{"api", "games", "gameID", "screen-plan", "step"}, ""))
pattern_EveningDetectiveServer_GetScreenTeams_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "screens", "teams"}, ""))
pattern_EveningDetectiveServer_GetScreenResults_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "screens", "results"}, "")) pattern_EveningDetectiveServer_GetScreenResults_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "screens", "results"}, ""))
pattern_EveningDetectiveServer_DownloadGamePrintMaterials_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "ID", "print-materials"}, "")) pattern_EveningDetectiveServer_DownloadGamePrintMaterials_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "ID", "print-materials"}, ""))
pattern_EveningDetectiveServer_PreviewTeamReceipt_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 1, 0, 4, 1, 5, 4, 2, 5}, []string{"api", "games", "gameID", "teams", "teamID", "receipt.png"}, "")) pattern_EveningDetectiveServer_PreviewTeamReceipt_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 1, 0, 4, 1, 5, 4, 2, 5}, []string{"api", "games", "gameID", "teams", "teamID", "receipt.png"}, ""))
@@ -9083,6 +9148,7 @@ var (
forward_EveningDetectiveServer_GetScreenPlan_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_GetScreenPlan_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_SaveScreenPlan_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_SaveScreenPlan_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_SetScreenPlanStep_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_SetScreenPlanStep_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_GetScreenTeams_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_GetScreenResults_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_GetScreenResults_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_DownloadGamePrintMaterials_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_DownloadGamePrintMaterials_0 = runtime.ForwardResponseMessage
forward_EveningDetectiveServer_PreviewTeamReceipt_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_PreviewTeamReceipt_0 = runtime.ForwardResponseMessage
+38
View File
@@ -112,6 +112,7 @@ const (
EveningDetectiveServer_GetScreenPlan_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetScreenPlan" EveningDetectiveServer_GetScreenPlan_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetScreenPlan"
EveningDetectiveServer_SaveScreenPlan_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/SaveScreenPlan" EveningDetectiveServer_SaveScreenPlan_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/SaveScreenPlan"
EveningDetectiveServer_SetScreenPlanStep_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/SetScreenPlanStep" EveningDetectiveServer_SetScreenPlanStep_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/SetScreenPlanStep"
EveningDetectiveServer_GetScreenTeams_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetScreenTeams"
EveningDetectiveServer_GetScreenResults_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetScreenResults" EveningDetectiveServer_GetScreenResults_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetScreenResults"
EveningDetectiveServer_DownloadGamePrintMaterials_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/DownloadGamePrintMaterials" EveningDetectiveServer_DownloadGamePrintMaterials_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/DownloadGamePrintMaterials"
EveningDetectiveServer_PreviewTeamReceipt_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/PreviewTeamReceipt" EveningDetectiveServer_PreviewTeamReceipt_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/PreviewTeamReceipt"
@@ -248,6 +249,7 @@ type EveningDetectiveServerClient interface {
GetScreenPlan(ctx context.Context, in *GetScreenPlanReq, opts ...grpc.CallOption) (*GetScreenPlanRsp, error) GetScreenPlan(ctx context.Context, in *GetScreenPlanReq, opts ...grpc.CallOption) (*GetScreenPlanRsp, error)
SaveScreenPlan(ctx context.Context, in *SaveScreenPlanReq, opts ...grpc.CallOption) (*SaveScreenPlanRsp, error) SaveScreenPlan(ctx context.Context, in *SaveScreenPlanReq, opts ...grpc.CallOption) (*SaveScreenPlanRsp, error)
SetScreenPlanStep(ctx context.Context, in *SetScreenPlanStepReq, opts ...grpc.CallOption) (*SetScreenPlanStepRsp, error) SetScreenPlanStep(ctx context.Context, in *SetScreenPlanStepReq, opts ...grpc.CallOption) (*SetScreenPlanStepRsp, error)
GetScreenTeams(ctx context.Context, in *GetScreenTeamsReq, opts ...grpc.CallOption) (*GetScreenTeamsRsp, error)
GetScreenResults(ctx context.Context, in *GetScreenResultsReq, opts ...grpc.CallOption) (*GetScreenResultsRsp, error) GetScreenResults(ctx context.Context, in *GetScreenResultsReq, opts ...grpc.CallOption) (*GetScreenResultsRsp, error)
DownloadGamePrintMaterials(ctx context.Context, in *GetGameReq, opts ...grpc.CallOption) (*httpbody.HttpBody, error) DownloadGamePrintMaterials(ctx context.Context, in *GetGameReq, opts ...grpc.CallOption) (*httpbody.HttpBody, error)
PreviewTeamReceipt(ctx context.Context, in *TeamReceiptReq, opts ...grpc.CallOption) (*httpbody.HttpBody, error) PreviewTeamReceipt(ctx context.Context, in *TeamReceiptReq, opts ...grpc.CallOption) (*httpbody.HttpBody, error)
@@ -1148,6 +1150,16 @@ func (c *eveningDetectiveServerClient) SetScreenPlanStep(ctx context.Context, in
return out, nil return out, nil
} }
func (c *eveningDetectiveServerClient) GetScreenTeams(ctx context.Context, in *GetScreenTeamsReq, opts ...grpc.CallOption) (*GetScreenTeamsRsp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetScreenTeamsRsp)
err := c.cc.Invoke(ctx, EveningDetectiveServer_GetScreenTeams_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *eveningDetectiveServerClient) GetScreenResults(ctx context.Context, in *GetScreenResultsReq, opts ...grpc.CallOption) (*GetScreenResultsRsp, error) { func (c *eveningDetectiveServerClient) GetScreenResults(ctx context.Context, in *GetScreenResultsReq, opts ...grpc.CallOption) (*GetScreenResultsRsp, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GetScreenResultsRsp) out := new(GetScreenResultsRsp)
@@ -1552,6 +1564,7 @@ type EveningDetectiveServerServer interface {
GetScreenPlan(context.Context, *GetScreenPlanReq) (*GetScreenPlanRsp, error) GetScreenPlan(context.Context, *GetScreenPlanReq) (*GetScreenPlanRsp, error)
SaveScreenPlan(context.Context, *SaveScreenPlanReq) (*SaveScreenPlanRsp, error) SaveScreenPlan(context.Context, *SaveScreenPlanReq) (*SaveScreenPlanRsp, error)
SetScreenPlanStep(context.Context, *SetScreenPlanStepReq) (*SetScreenPlanStepRsp, error) SetScreenPlanStep(context.Context, *SetScreenPlanStepReq) (*SetScreenPlanStepRsp, error)
GetScreenTeams(context.Context, *GetScreenTeamsReq) (*GetScreenTeamsRsp, error)
GetScreenResults(context.Context, *GetScreenResultsReq) (*GetScreenResultsRsp, error) GetScreenResults(context.Context, *GetScreenResultsReq) (*GetScreenResultsRsp, error)
DownloadGamePrintMaterials(context.Context, *GetGameReq) (*httpbody.HttpBody, error) DownloadGamePrintMaterials(context.Context, *GetGameReq) (*httpbody.HttpBody, error)
PreviewTeamReceipt(context.Context, *TeamReceiptReq) (*httpbody.HttpBody, error) PreviewTeamReceipt(context.Context, *TeamReceiptReq) (*httpbody.HttpBody, error)
@@ -1850,6 +1863,9 @@ func (UnimplementedEveningDetectiveServerServer) SaveScreenPlan(context.Context,
func (UnimplementedEveningDetectiveServerServer) SetScreenPlanStep(context.Context, *SetScreenPlanStepReq) (*SetScreenPlanStepRsp, error) { func (UnimplementedEveningDetectiveServerServer) SetScreenPlanStep(context.Context, *SetScreenPlanStepReq) (*SetScreenPlanStepRsp, error) {
return nil, status.Error(codes.Unimplemented, "method SetScreenPlanStep not implemented") return nil, status.Error(codes.Unimplemented, "method SetScreenPlanStep not implemented")
} }
func (UnimplementedEveningDetectiveServerServer) GetScreenTeams(context.Context, *GetScreenTeamsReq) (*GetScreenTeamsRsp, error) {
return nil, status.Error(codes.Unimplemented, "method GetScreenTeams not implemented")
}
func (UnimplementedEveningDetectiveServerServer) GetScreenResults(context.Context, *GetScreenResultsReq) (*GetScreenResultsRsp, error) { func (UnimplementedEveningDetectiveServerServer) GetScreenResults(context.Context, *GetScreenResultsReq) (*GetScreenResultsRsp, error) {
return nil, status.Error(codes.Unimplemented, "method GetScreenResults not implemented") return nil, status.Error(codes.Unimplemented, "method GetScreenResults not implemented")
} }
@@ -3510,6 +3526,24 @@ func _EveningDetectiveServer_SetScreenPlanStep_Handler(srv interface{}, ctx cont
return interceptor(ctx, in, info, handler) return interceptor(ctx, in, info, handler)
} }
func _EveningDetectiveServer_GetScreenTeams_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetScreenTeamsReq)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(EveningDetectiveServerServer).GetScreenTeams(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: EveningDetectiveServer_GetScreenTeams_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(EveningDetectiveServerServer).GetScreenTeams(ctx, req.(*GetScreenTeamsReq))
}
return interceptor(ctx, in, info, handler)
}
func _EveningDetectiveServer_GetScreenResults_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { func _EveningDetectiveServer_GetScreenResults_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(GetScreenResultsReq) in := new(GetScreenResultsReq)
if err := dec(in); err != nil { if err := dec(in); err != nil {
@@ -4401,6 +4435,10 @@ var EveningDetectiveServer_ServiceDesc = grpc.ServiceDesc{
MethodName: "SetScreenPlanStep", MethodName: "SetScreenPlanStep",
Handler: _EveningDetectiveServer_SetScreenPlanStep_Handler, Handler: _EveningDetectiveServer_SetScreenPlanStep_Handler,
}, },
{
MethodName: "GetScreenTeams",
Handler: _EveningDetectiveServer_GetScreenTeams_Handler,
},
{ {
MethodName: "GetScreenResults", MethodName: "GetScreenResults",
Handler: _EveningDetectiveServer_GetScreenResults_Handler, Handler: _EveningDetectiveServer_GetScreenResults_Handler,