diff --git a/.env.example b/.env.example index b5c7ba1..beb0685 100644 --- a/.env.example +++ b/.env.example @@ -14,8 +14,9 @@ SMTP_TIMEOUT=10s JWT_SECRET=your_secret -# Публичный адрес фронтенда: используется для ссылки «Войти в игру» в -# письмах. Если не задан или не http(s) — ссылка в письма не добавляется. +# Публичный адрес приложения: ссылки «Войти в игру» в письмах и QR-коды команд. +# Должен совпадать с базовым адресом существующих QR-кодов фронтенда. +# Если не задан или не http(s), PDF команд недоступен, ссылка в письма не добавляется. APP_BASE_URL=https://evening-detective.crabs-games.art S3_HOST=http://0.0.0.0:9000 diff --git a/README.md b/README.md index 48b9caf..13f1ec0 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,16 @@ goose -dir ./migrations postgres "user=postgres password=postgres host=localhost make test ``` +## PDF с QR-кодами команд + +Кнопка «QR команд» на странице управления игрой скачивает `GET /api/games/{id}/teams/qr.pdf` с авторизацией организатора, автора или администратора. Без команд кнопка скрыта; прямой запрос для пустой игры возвращает `FailedPrecondition`. + +Модуль `internal/modules/team_qr_pdf` формирует A4 в книжной ориентации, 12 карточек (3 × 4) с названиями команд и пунктирными линиями для разрезания. Имя файла: `YYYY-MM-DD_Название игры.pdf`, дата берётся из `games.start_at`. Недопустимые символы имени файла заменяются на `_`. + +Для рабочих ссылок задайте `APP_BASE_URL` — тот же публичный адрес приложения, который используется существующими QR-кодами фронтенда (`VITE_API_URL`). Пустой или некорректный адрес приводит к ошибке скачивания. PDF не сохраняется в хранилище и отдаётся с `Cache-Control: no-store`. + +Шрифт DejaVu Sans Condensed встроен в бинарник вместе с лицензией; системные шрифты не нужны. Поддерживается кириллица, длинные названия переносятся и уменьшаются до размера 6 pt. Символы вне BMP (например, emoji) вызывают явную ошибку генерации из-за ограничения PDF-библиотеки. + ## Закрывашка сценария Автор (или администратор) сохраняет закрывашку через `PUT /api/scenarios/{id}/conclusion`: diff --git a/api/main.proto b/api/main.proto index 7f28583..910759a 100644 --- a/api/main.proto +++ b/api/main.proto @@ -484,6 +484,16 @@ service EveningDetectiveServer { }; } + rpc DownloadTeamQR(GetGameReq) returns (google.api.HttpBody) { + option (google.api.http) = { + get: "/api/games/{id}/teams/qr.pdf" + }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + tags : "Игры"; + summary: "Скачать PDF с QR-кодами всех команд игры"; + }; + } + rpc GetGameAnswers(GetGameAnswersReq) returns (GetGameAnswersRsp) { option (google.api.http) = { get: "/api/games/{gameId}/answers" diff --git a/bin/evening_detective_server b/bin/evening_detective_server index 5d3e01a..64f9a1f 100755 Binary files a/bin/evening_detective_server and b/bin/evening_detective_server differ diff --git a/cmd/evening_detective_server/gateway_test.go b/cmd/evening_detective_server/gateway_test.go index 32fdb14..2722ae8 100644 --- a/cmd/evening_detective_server/gateway_test.go +++ b/cmd/evening_detective_server/gateway_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "io" + "mime" "net" "net/http" "net/http/httptest" @@ -13,6 +14,7 @@ import ( "testing" "evening_detective_server/internal/modules/scenario_archive" + "evening_detective_server/internal/modules/team_qr_pdf" proto "evening_detective_server/proto" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" @@ -78,6 +80,54 @@ func (s *testStub) getUploaded() []byte { return append([]byte(nil), s.uploaded...) } +func (s *testStub) DownloadTeamQR(ctx context.Context, req *proto.GetGameReq) (*httpbody.HttpBody, error) { + if req.Id != 7 { + return nil, status.Error(codes.NotFound, "Игра не найдена") + } + data, err := team_qr_pdf.NewGenerator().Generate([]team_qr_pdf.Card{{Name: "Знатоки", URL: "https://example.org/team-story/1?password=test"}}) + if err != nil { + return nil, err + } + if err := grpc.SetHeader(ctx, metadata.Pairs( + "Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": "2026-09-08_Дело №1.pdf"}), + "Cache-Control", "no-store", + )); err != nil { + return nil, err + } + return &httpbody.HttpBody{Data: data, ContentType: "application/pdf"}, nil +} + +func TestGatewayDownloadTeamQR(t *testing.T) { + ts := newTestGateway(t, &testStub{}) + resp, err := http.Get(ts.URL + "/api/games/7/teams/qr.pdf") + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + data, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if resp.StatusCode != http.StatusOK || resp.Header.Get("Content-Type") != "application/pdf" || !bytes.HasPrefix(data, []byte("%PDF-")) { + t.Fatalf("response: %d, %v", resp.StatusCode, resp.Header) + } + mediaType, params, err := mime.ParseMediaType(resp.Header.Get("Content-Disposition")) + if err != nil || mediaType != "attachment" || params["filename"] != "2026-09-08_Дело №1.pdf" { + t.Fatalf("filename header: %v (%v)", resp.Header, err) + } + if resp.Header.Get("Cache-Control") != "no-store" { + t.Fatal("PDF with passwords must not be cached") + } + resp2, err := http.Get(ts.URL + "/api/games/8/teams/qr.pdf") + if err != nil { + t.Fatal(err) + } + defer resp2.Body.Close() + if resp2.StatusCode != http.StatusNotFound { + t.Fatalf("missing game: %d", resp2.StatusCode) + } +} + // newTestGateway поднимает gRPC-сервер со stub и grpc-gateway с теми же // опциями, что и в main.go (outgoing matcher, error handler, лимиты, // rawBodyMarshaler). @@ -117,6 +167,8 @@ func newTestGatewayWithLimit(t *testing.T, stub proto.EveningDetectiveServerServ return "Content-Disposition", true case strings.EqualFold(key, "X-Content-Type-Options"): return "X-Content-Type-Options", true + case strings.EqualFold(key, "Cache-Control"): + return "Cache-Control", true } return runtime.DefaultHeaderMatcher(key) }), diff --git a/cmd/evening_detective_server/main.go b/cmd/evening_detective_server/main.go index d6dd8c0..82c773c 100644 --- a/cmd/evening_detective_server/main.go +++ b/cmd/evening_detective_server/main.go @@ -13,6 +13,7 @@ import ( "evening_detective_server/internal/modules/scenario_archive" "evening_detective_server/internal/modules/storytelling" "evening_detective_server/internal/modules/string_tools" + "evening_detective_server/internal/modules/team_qr_pdf" "evening_detective_server/internal/repos/actions_repo" "evening_detective_server/internal/repos/applications_repo" "evening_detective_server/internal/repos/games_repo" @@ -139,6 +140,8 @@ func main() { applicationsRepo, answersRepo, cleaner, + team_qr_pdf.NewGenerator(), + os.Getenv("APP_BASE_URL"), ) // Create a listener on TCP port @@ -239,6 +242,8 @@ func main() { return "Content-Disposition", true case strings.EqualFold(key, "X-Content-Type-Options"): return "X-Content-Type-Options", true + case strings.EqualFold(key, "Cache-Control"): + return "Cache-Control", true } return runtime.DefaultHeaderMatcher(key) }), diff --git a/cmd/evening_detective_server/main.swagger.json b/cmd/evening_detective_server/main.swagger.json index 63ca105..bc391e6 100644 --- a/cmd/evening_detective_server/main.swagger.json +++ b/cmd/evening_detective_server/main.swagger.json @@ -720,6 +720,38 @@ ] } }, + "/api/games/{id}/teams/qr.pdf": { + "get": { + "summary": "Скачать PDF с QR-кодами всех команд игры", + "operationId": "EveningDetectiveServer_DownloadTeamQR", + "responses": { + "200": { + "description": "A successful response.", + "schema": { + "$ref": "#/definitions/apiHttpBody" + } + }, + "default": { + "description": "An unexpected error response.", + "schema": { + "$ref": "#/definitions/rpcStatus" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "integer", + "format": "int32" + } + ], + "tags": [ + "Игры" + ] + } + }, "/api/my-scenarios": { "get": { "summary": "Получить свои сценарии", diff --git a/go.mod b/go.mod index 003ebd7..ff28b72 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,10 @@ module evening_detective_server go 1.26 require ( + github.com/go-pdf/fpdf v0.9.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.30.0 github.com/mark3labs/mcp-go v0.58.0 + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/stretchr/testify v1.12.0 google.golang.org/genproto/googleapis/api v0.0.0-20260810153831-ec0a7760b754 google.golang.org/grpc v1.83.0 diff --git a/internal/app/team_qr.go b/internal/app/team_qr.go new file mode 100644 index 0000000..1cc6500 --- /dev/null +++ b/internal/app/team_qr.go @@ -0,0 +1,51 @@ +package app + +import ( + "context" + "errors" + "mime" + + "evening_detective_server/internal/modules/processor_jwt" + "evening_detective_server/internal/modules/roles" + "evening_detective_server/internal/modules/team_qr_pdf" + "evening_detective_server/internal/repos/games_repo" + "evening_detective_server/proto" + + "google.golang.org/genproto/googleapis/api/httpbody" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" +) + +func (s *server) DownloadTeamQR(ctx context.Context, req *proto.GetGameReq) (*httpbody.HttpBody, error) { + claims, ok := ctx.Value("claims").(*processor_jwt.JWTClaims) + if !ok || claims == nil { + return nil, status.Error(codes.Unauthenticated, "необходима авторизация") + } + if !roles.HasRole(claims, roles.Organizer) && !roles.HasRole(claims, roles.Author) { + return nil, status.Error(codes.PermissionDenied, "permission denied") + } + if req == nil || req.Id <= 0 { + return nil, status.Error(codes.InvalidArgument, "некорректный идентификатор игры") + } + data, filename, err := s.gameService.DownloadTeamQR(ctx, int(req.Id)) + if err != nil { + switch { + case errors.Is(err, games_repo.ErrGameNotFound): + return nil, status.Error(codes.NotFound, err.Error()) + case errors.Is(err, team_qr_pdf.ErrNoTeams): + return nil, status.Error(codes.FailedPrecondition, err.Error()) + default: + return nil, status.Error(codes.Internal, err.Error()) + } + } + if err := grpc.SetHeader(ctx, metadata.Pairs( + "Content-Disposition", mime.FormatMediaType("attachment", map[string]string{"filename": filename}), + "X-Content-Type-Options", "nosniff", + "Cache-Control", "no-store", + )); err != nil { + return nil, status.Error(codes.Internal, "не удалось установить заголовки PDF") + } + return &httpbody.HttpBody{ContentType: "application/pdf", Data: data}, nil +} diff --git a/internal/app/team_qr_test.go b/internal/app/team_qr_test.go new file mode 100644 index 0000000..0dbfa23 --- /dev/null +++ b/internal/app/team_qr_test.go @@ -0,0 +1,30 @@ +package app + +import ( + "context" + "testing" + + "evening_detective_server/proto" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +func TestDownloadTeamQRAccess(t *testing.T) { + s := &server{} + for _, tc := range []struct { + ctx context.Context + id int32 + want codes.Code + }{ + {context.Background(), 1, codes.Unauthenticated}, + {ctxWithClaims(), 1, codes.PermissionDenied}, + {ctxWithClaims("organizer"), 0, codes.InvalidArgument}, + {ctxWithClaims("author"), -1, codes.InvalidArgument}, + {ctxWithClaims("admin"), 0, codes.InvalidArgument}, + } { + _, err := s.DownloadTeamQR(tc.ctx, &proto.GetGameReq{Id: tc.id}) + if status.Code(err) != tc.want { + t.Errorf("code = %v, want %v", status.Code(err), tc.want) + } + } +} diff --git a/internal/modules/team_qr_pdf/fonts/DejaVuSansCondensed.ttf b/internal/modules/team_qr_pdf/fonts/DejaVuSansCondensed.ttf new file mode 100644 index 0000000..3259bc2 Binary files /dev/null and b/internal/modules/team_qr_pdf/fonts/DejaVuSansCondensed.ttf differ diff --git a/internal/modules/team_qr_pdf/fonts/LICENSE b/internal/modules/team_qr_pdf/fonts/LICENSE new file mode 100644 index 0000000..df52c17 --- /dev/null +++ b/internal/modules/team_qr_pdf/fonts/LICENSE @@ -0,0 +1,187 @@ +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. +Glyphs imported from Arev fonts are (c) Tavmjong Bah (see below) + + +Bitstream Vera Fonts Copyright +------------------------------ + +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera is +a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license ("Fonts") and associated +documentation files (the "Font Software"), to reproduce and distribute the +Font Software, including without limitation the rights to use, copy, merge, +publish, distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to the +following conditions: + +The above copyright and trademark notices and this permission notice shall +be included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional glyphs or characters may be added to the Fonts, only if the fonts +are renamed to names not containing either the words "Bitstream" or the word +"Vera". + +This License becomes null and void to the extent applicable to Fonts or Font +Software that has been modified and is distributed under the "Bitstream +Vera" names. + +The Font Software may be sold as part of a larger software package but no +copy of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING +ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF +THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE +FONT SOFTWARE. + +Except as contained in this notice, the names of Gnome, the Gnome +Foundation, and Bitstream Inc., shall not be used in advertising or +otherwise to promote the sale, use or other dealings in this Font Software +without prior written authorization from the Gnome Foundation or Bitstream +Inc., respectively. For further information, contact: fonts at gnome dot +org. + +Arev Fonts Copyright +------------------------------ + +Copyright (c) 2006 by Tavmjong Bah. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of the fonts accompanying this license ("Fonts") and +associated documentation files (the "Font Software"), to reproduce +and distribute the modifications to the Bitstream Vera Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, and/or sell copies of the Font Software, and to permit +persons to whom the Font Software is furnished to do so, subject to +the following conditions: + +The above copyright and trademark notices and this permission notice +shall be included in all copies of one or more of the Font Software +typefaces. + +The Font Software may be modified, altered, or added to, and in +particular the designs of glyphs or characters in the Fonts may be +modified and additional glyphs or characters may be added to the +Fonts, only if the fonts are renamed to names not containing either +the words "Tavmjong Bah" or the word "Arev". + +This License becomes null and void to the extent applicable to Fonts +or Font Software that has been modified and is distributed under the +"Tavmjong Bah Arev" names. + +The Font Software may be sold as part of a larger software package but +no copy of one or more of the Font Software typefaces may be sold by +itself. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL +TAVMJONG BAH BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. + +Except as contained in this notice, the name of Tavmjong Bah shall not +be used in advertising or otherwise to promote the sale, use or other +dealings in this Font Software without prior written authorization +from Tavmjong Bah. For further information, contact: tavmjong @ free +. fr. + +TeX Gyre DJV Math +----------------- +Fonts are (c) Bitstream (see below). DejaVu changes are in public domain. + +Math extensions done by B. Jackowski, P. Strzelczyk and P. Pianowski +(on behalf of TeX users groups) are in public domain. + +Letters imported from Euler Fraktur from AMSfonts are (c) American +Mathematical Society (see below). +Bitstream Vera Fonts Copyright +Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. Bitstream Vera +is a trademark of Bitstream, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of the fonts accompanying this license (“Fonts”) and associated +documentation +files (the “Font Software”), to reproduce and distribute the Font Software, +including without limitation the rights to use, copy, merge, publish, +distribute, +and/or sell copies of the Font Software, and to permit persons to whom +the Font Software is furnished to do so, subject to the following +conditions: + +The above copyright and trademark notices and this permission notice +shall be +included in all copies of one or more of the Font Software typefaces. + +The Font Software may be modified, altered, or added to, and in particular +the designs of glyphs or characters in the Fonts may be modified and +additional +glyphs or characters may be added to the Fonts, only if the fonts are +renamed +to names not containing either the words “Bitstream” or the word “Vera”. + +This License becomes null and void to the extent applicable to Fonts or +Font Software +that has been modified and is distributed under the “Bitstream Vera” +names. + +The Font Software may be sold as part of a larger software package but +no copy +of one or more of the Font Software typefaces may be sold by itself. + +THE FONT SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME +FOUNDATION +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, +SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN +ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR +INABILITY TO USE +THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. +Except as contained in this notice, the names of GNOME, the GNOME +Foundation, +and Bitstream Inc., shall not be used in advertising or otherwise to promote +the sale, use or other dealings in this Font Software without prior written +authorization from the GNOME Foundation or Bitstream Inc., respectively. +For further information, contact: fonts at gnome dot org. + +AMSFonts (v. 2.2) copyright + +The PostScript Type 1 implementation of the AMSFonts produced by and +previously distributed by Blue Sky Research and Y&Y, Inc. are now freely +available for general use. This has been accomplished through the +cooperation +of a consortium of scientific publishers with Blue Sky Research and Y&Y. +Members of this consortium include: + +Elsevier Science IBM Corporation Society for Industrial and Applied +Mathematics (SIAM) Springer-Verlag American Mathematical Society (AMS) + +In order to assure the authenticity of these fonts, copyright will be +held by +the American Mathematical Society. This is not meant to restrict in any way +the legitimate use of the fonts, such as (but not limited to) electronic +distribution of documents containing these fonts, inclusion of these fonts +into other public domain or commercial font collections or computer +applications, use of the outline data to create derivative fonts and/or +faces, etc. However, the AMS does require that the AMS copyright notice be +removed from any derivative versions of the fonts which have been altered in +any way. In addition, to ensure the fidelity of TeX documents using Computer +Modern fonts, Professor Donald Knuth, creator of the Computer Modern faces, +has requested that any alterations which yield different font metrics be +given a different name. + +$Id$ diff --git a/internal/modules/team_qr_pdf/generator.go b/internal/modules/team_qr_pdf/generator.go new file mode 100644 index 0000000..04728ff --- /dev/null +++ b/internal/modules/team_qr_pdf/generator.go @@ -0,0 +1,98 @@ +package team_qr_pdf + +import ( + "bytes" + _ "embed" + "fmt" + "net/url" + "strings" + + "github.com/go-pdf/fpdf" + "github.com/skip2/go-qrcode" +) + +//go:embed fonts/DejaVuSansCondensed.ttf +var font []byte + +type generator struct{} + +func NewGenerator() IGenerator { return &generator{} } + +func (g *generator) Generate(cards []Card) ([]byte, error) { + if len(cards) == 0 { + return nil, ErrNoTeams + } + pdf := fpdf.New("P", "mm", "A4", "") + pdf.SetAutoPageBreak(false, 0) + pdf.SetCellMargin(0) + pdf.AddUTF8FontFromBytes("teams", "", font) + pdf.SetFillColor(0, 0, 0) + const margin, width, height, qrSize = 10.0, 190.0 / 3, 277.0 / 4, 36.0 + for i, card := range cards { + u, err := url.Parse(card.URL) + if err != nil || (u.Scheme != "http" && u.Scheme != "https") || u.Host == "" { + return nil, fmt.Errorf("некорректная ссылка команды %d", i+1) + } + qr, err := qrcode.New(card.URL, qrcode.Medium) + if err != nil { + return nil, fmt.Errorf("QR команды %d: %w", i+1, err) + } + if i%12 == 0 { + pdf.AddPage() + pdf.SetDrawColor(100, 100, 100) + pdf.SetLineWidth(0.2) + pdf.SetDashPattern([]float64{1, 1}, 0) + for col := 0; col <= 3; col++ { + x := margin + float64(col)*width + pdf.Line(x, margin, x, margin+4*height) + } + for row := 0; row <= 4; row++ { + y := margin + float64(row)*height + pdf.Line(margin, y, margin+3*width, y) + } + } + x := margin + float64(i%3)*width + y := margin + float64(i%12/3)*height + // Bitmap включает свободное поле QR; векторные квадраты сохраняют + // чёткость при печати без растрового масштабирования. + bitmap := qr.Bitmap() + pixel := qrSize / float64(len(bitmap)) + for row, cells := range bitmap { + for col, black := range cells { + if black { + pdf.Rect(x+(width-qrSize)/2+float64(col)*pixel, y+3+float64(row)*pixel, pixel, pixel, "F") + } + } + } + name := strings.Join(strings.Fields(card.Name), " ") + // ponytail: fpdf поддерживает BMP; для символов вне BMP (например, + // emoji) нужен другой движок шрифтов. Возвращаем ошибку вместо паники. + for _, r := range name { + if r > 0xffff { + return nil, fmt.Errorf("название команды %d содержит неподдерживаемый символ %q", i+1, r) + } + } + var lines []string + var lineHeight float64 + for size := 12.0; size >= 6; size-- { + pdf.SetFont("teams", "", size) + lineHeight = size * 0.3528 * 1.15 + lines = pdf.SplitText(name, width-6) + if float64(len(lines))*lineHeight <= height-qrSize-8 { + break + } + } + if float64(len(lines))*lineHeight > height-qrSize-8 { + return nil, fmt.Errorf("название команды %d не помещается на карточке (%d строк, %f мм)", i+1, len(lines), lineHeight) + } + for line, text := range lines { + pdf.SetXY(x+3, y+qrSize+5+float64(line)*lineHeight) + pdf.CellFormat(width-6, lineHeight, text, "", 0, "C", false, 0, "") + } + } + var result bytes.Buffer + if err := pdf.Output(&result); err != nil { + return nil, err + } + return result.Bytes(), nil +} diff --git a/internal/modules/team_qr_pdf/generator_test.go b/internal/modules/team_qr_pdf/generator_test.go new file mode 100644 index 0000000..dde16e9 --- /dev/null +++ b/internal/modules/team_qr_pdf/generator_test.go @@ -0,0 +1,55 @@ +package team_qr_pdf + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strings" + "testing" +) + +func TestGenerate(t *testing.T) { + g := NewGenerator() + if _, err := g.Generate(nil); !errors.Is(err, ErrNoTeams) { + t.Fatalf("empty game: %v", err) + } + for _, count := range []int{1, 12, 13, 24} { + t.Run(fmt.Sprint(count), func(t *testing.T) { + cards := make([]Card, count) + for i := range cards { + cards[i] = Card{ + Name: fmt.Sprintf("Команда №%d: Следствие ведут знатоки", i+1), + URL: fmt.Sprintf("https://example.org/team-story/%d?password=abc123", i+1), + } + } + cards[0].Name = strings.Repeat("Ш", 255) + data, err := g.Generate(cards) + if err != nil { + t.Fatal(err) + } + if !bytes.HasPrefix(data, []byte("%PDF-")) || !bytes.Contains(data, []byte("%%EOF")) { + t.Fatal("invalid PDF") + } + pages := regexp.MustCompile(`/Type /Page\b`).FindAll(data, -1) + if len(pages) != (count+11)/12 { + t.Fatalf("pages = %d for %d teams", len(pages), count) + } + if !bytes.Contains(data, []byte("/MediaBox [0 0 595.28 841.89]")) { + t.Fatal("page is not A4 portrait") + } + if !bytes.Contains(data, []byte("/FontFile2")) { + t.Fatal("font is not embedded") + } + }) + } + for _, card := range []Card{ + {Name: "Команда", URL: "javascript:alert(1)"}, + {Name: "Команда", URL: "https://example.org/" + strings.Repeat("x", 5000)}, + {Name: "Команда 🔍", URL: "https://example.org"}, + } { + if _, err := g.Generate([]Card{card}); err == nil { + t.Fatal("expected error for unsupported input") + } + } +} diff --git a/internal/modules/team_qr_pdf/interface.go b/internal/modules/team_qr_pdf/interface.go new file mode 100644 index 0000000..6c6de6c --- /dev/null +++ b/internal/modules/team_qr_pdf/interface.go @@ -0,0 +1,14 @@ +package team_qr_pdf + +import "errors" + +var ErrNoTeams = errors.New("в игре нет команд") + +type Card struct { + Name string + URL string +} + +type IGenerator interface { + Generate(cards []Card) ([]byte, error) +} diff --git a/internal/services/game_service/service.go b/internal/services/game_service/service.go index 61a4697..50c9523 100644 --- a/internal/services/game_service/service.go +++ b/internal/services/game_service/service.go @@ -6,6 +6,7 @@ import ( "evening_detective_server/internal/modules/cleaner" "evening_detective_server/internal/modules/password_generator" "evening_detective_server/internal/modules/storytelling" + "evening_detective_server/internal/modules/team_qr_pdf" "evening_detective_server/internal/repos" "evening_detective_server/internal/repos/actions_repo" "evening_detective_server/internal/repos/applications_repo" @@ -29,6 +30,8 @@ type GameService struct { applicationsRepo *applications_repo.ApplicationsRepo answersRepo *team_answers_repo.TeamAnswersRepo cleaner cleaner.ICleaner + teamQRGenerator team_qr_pdf.IGenerator + appBaseURL string } func NewGameService( @@ -43,6 +46,8 @@ func NewGameService( applicationsRepo *applications_repo.ApplicationsRepo, answersRepo *team_answers_repo.TeamAnswersRepo, cleaner cleaner.ICleaner, + teamQRGenerator team_qr_pdf.IGenerator, + appBaseURL string, ) *GameService { return &GameService{ gamesRepo: gamesRepo, @@ -56,6 +61,8 @@ func NewGameService( applicationsRepo: applicationsRepo, answersRepo: answersRepo, cleaner: cleaner, + teamQRGenerator: teamQRGenerator, + appBaseURL: appBaseURL, } } diff --git a/internal/services/game_service/team_qr.go b/internal/services/game_service/team_qr.go new file mode 100644 index 0000000..4d3c9d8 --- /dev/null +++ b/internal/services/game_service/team_qr.go @@ -0,0 +1,58 @@ +package game_service + +import ( + "context" + "errors" + "fmt" + "net/url" + "strings" + "unicode" + + "evening_detective_server/internal/modules/team_qr_pdf" + "evening_detective_server/internal/repos" +) + +func (s *GameService) DownloadTeamQR(ctx context.Context, id int) ([]byte, string, error) { + game, err := s.gamesRepo.GetGameByID(ctx, id) + if err != nil { + return nil, "", err + } + teams, err := s.teamsRepo.GetTeamsByGameID(ctx, id) + if err != nil { + return nil, "", err + } + if len(teams) == 0 { + return nil, "", team_qr_pdf.ErrNoTeams + } + cards, filename, err := teamQRData(s.appBaseURL, game, teams) + if err != nil { + return nil, "", err + } + data, err := s.teamQRGenerator.Generate(cards) + return data, filename, err +} + +func teamQRData(baseURL string, game *repos.Game, teams []*repos.Team) ([]team_qr_pdf.Card, string, error) { + base, err := url.Parse(strings.TrimSpace(baseURL)) + if err != nil || (base.Scheme != "http" && base.Scheme != "https") || base.Host == "" || base.User != nil || base.RawQuery != "" || base.Fragment != "" { + return nil, "", errors.New("для QR-кодов необходимо настроить APP_BASE_URL — публичный адрес приложения") + } + cards := make([]team_qr_pdf.Card, 0, len(teams)) + for _, team := range teams { + if team.ID <= 0 || team.Password == "" { + return nil, "", errors.New("у команды отсутствуют данные для входа") + } + link := *base + link.Path = strings.TrimRight(base.Path, "/") + fmt.Sprintf("/team-story/%d", team.ID) + link.RawPath = "" + link.RawQuery = url.Values{"password": {team.Password}}.Encode() + cards = append(cards, team_qr_pdf.Card{Name: team.Name, URL: link.String()}) + } + name := strings.Map(func(r rune) rune { + if unicode.IsControl(r) || strings.ContainsRune(`\/:*?"<>|`, r) { + return '_' + } + return r + }, game.Name) + return cards, game.StartAt.Format("2006-01-02") + "_" + strings.TrimSpace(name) + ".pdf", nil +} diff --git a/internal/services/game_service/team_qr_test.go b/internal/services/game_service/team_qr_test.go new file mode 100644 index 0000000..ea2d18b --- /dev/null +++ b/internal/services/game_service/team_qr_test.go @@ -0,0 +1,37 @@ +package game_service + +import ( + "net/url" + "testing" + "time" + + "evening_detective_server/internal/repos" +) + +func TestTeamQRData(t *testing.T) { + game := &repos.Game{Name: "Дело / №1\r\n", StartAt: time.Date(2026, 9, 8, 14, 0, 0, 0, time.UTC)} + teams := []*repos.Team{{ID: 7, Name: "Знатоки", Password: "a+b&c?"}} + cards, filename, err := teamQRData("https://example.org/app/", game, teams) + if err != nil { + t.Fatal(err) + } + if len(cards) != 1 || cards[0].Name != "Знатоки" { + t.Fatalf("cards: %+v", cards) + } + u, err := url.Parse(cards[0].URL) + if err != nil || u.Path != "/app/team-story/7" || u.Query().Get("password") != teams[0].Password { + t.Fatalf("wrong team link: %s", cards[0].URL) + } + if filename != "2026-09-08_Дело _ №1__.pdf" { + t.Fatalf("filename: %s", filename) + } + for _, base := range []string{"", "ftp://example.org", "https://user:pass@example.org", "https://example.org?q=1", "https://example.org/#hash"} { + if _, _, err := teamQRData(base, game, teams); err == nil { + t.Errorf("accepted invalid base URL %q", base) + } + } + teams[0].Password = "" + if _, _, err := teamQRData("https://example.org", game, teams); err == nil { + t.Fatal("accepted missing password") + } +} diff --git a/proto/main.pb.go b/proto/main.pb.go index 6cfaccf..56cc110 100644 --- a/proto/main.pb.go +++ b/proto/main.pb.go @@ -7240,7 +7240,7 @@ const file_main_proto_rawDesc = "" + "background\x18\x02 \x01(\tR\n" + "background\"3\n" + "\x1bUpdateScenarioBackgroundRsp\x12\x14\n" + - "\x05error\x18\x01 \x01(\tR\x05error2\xf9j\n" + + "\x05error\x18\x01 \x01(\tR\x05error2\xc7l\n" + "\x16EveningDetectiveServer\x12\xa6\x01\n" + "\x04Ping\x12'.crabs.evening_detective_server.PingReq\x1a'.crabs.evening_detective_server.PingRsp\"L\x92A3\x12)Проверить доступностьb\x06\n" + "\x04\n" + @@ -7342,7 +7342,9 @@ const file_main_proto_rawDesc = "" + "\bИгры\x12\x19Получить игры\x82\xd3\xe4\x93\x02\f\x12\n" + "/api/games\x12\xa2\x01\n" + "\aGetGame\x12*.crabs.evening_detective_server.GetGameReq\x1a*.crabs.evening_detective_server.GetGameRsp\"?\x92A%\n" + - "\bИгры\x12\x19Получить игру\x82\xd3\xe4\x93\x02\x11\x12\x0f/api/games/{id}\x12\xa4\x02\n" + + "\bИгры\x12\x19Получить игру\x82\xd3\xe4\x93\x02\x11\x12\x0f/api/games/{id}\x12\xcb\x01\n" + + "\x0eDownloadTeamQR\x12*.crabs.evening_detective_server.GetGameReq\x1a\x14.google.api.HttpBody\"w\x92AP\n" + + "\bИгры\x12DСкачать PDF с QR-кодами всех команд игры\x82\xd3\xe4\x93\x02\x1e\x12\x1c/api/games/{id}/teams/qr.pdf\x12\xa4\x02\n" + "\x0eGetGameAnswers\x121.crabs.evening_detective_server.GetGameAnswersReq\x1a1.crabs.evening_detective_server.GetGameAnswersRsp\"\xab\x01\x92A\x84\x01\n" + "\bИгры\x12xПолучить ответы команд на вопросы дела (для оценки организатором)\x82\xd3\xe4\x93\x02\x1d\x12\x1b/api/games/{gameId}/answers\x12\x86\x02\n" + "\x0eGetGameResults\x121.crabs.evening_detective_server.GetGameResultsReq\x1a1.crabs.evening_detective_server.GetGameResultsRsp\"\x8d\x01\x92Ag\n" + @@ -7644,87 +7646,89 @@ var file_main_proto_depIdxs = []int32{ 82, // 87: crabs.evening_detective_server.EveningDetectiveServer.AddGame:input_type -> crabs.evening_detective_server.AddGameReq 84, // 88: crabs.evening_detective_server.EveningDetectiveServer.GetGames:input_type -> crabs.evening_detective_server.GetGamesReq 88, // 89: crabs.evening_detective_server.EveningDetectiveServer.GetGame:input_type -> crabs.evening_detective_server.GetGameReq - 90, // 90: crabs.evening_detective_server.EveningDetectiveServer.GetGameAnswers:input_type -> crabs.evening_detective_server.GetGameAnswersReq - 92, // 91: crabs.evening_detective_server.EveningDetectiveServer.GetGameResults:input_type -> crabs.evening_detective_server.GetGameResultsReq - 96, // 92: crabs.evening_detective_server.EveningDetectiveServer.UpdateGame:input_type -> crabs.evening_detective_server.UpdateGameReq - 98, // 93: crabs.evening_detective_server.EveningDetectiveServer.StartGame:input_type -> crabs.evening_detective_server.StartGameReq - 100, // 94: crabs.evening_detective_server.EveningDetectiveServer.PauseGame:input_type -> crabs.evening_detective_server.PauseGameReq - 102, // 95: crabs.evening_detective_server.EveningDetectiveServer.PlayGame:input_type -> crabs.evening_detective_server.PlayGameReq - 104, // 96: crabs.evening_detective_server.EveningDetectiveServer.FinishGame:input_type -> crabs.evening_detective_server.FinishGameReq - 106, // 97: crabs.evening_detective_server.EveningDetectiveServer.ResetGame:input_type -> crabs.evening_detective_server.ResetGameReq - 108, // 98: crabs.evening_detective_server.EveningDetectiveServer.DeleteGame:input_type -> crabs.evening_detective_server.DeleteGameReq - 110, // 99: crabs.evening_detective_server.EveningDetectiveServer.AddTeam:input_type -> crabs.evening_detective_server.AddTeamReq - 112, // 100: crabs.evening_detective_server.EveningDetectiveServer.UpdateTeam:input_type -> crabs.evening_detective_server.UpdateTeamReq - 114, // 101: crabs.evening_detective_server.EveningDetectiveServer.DeleteTeam:input_type -> crabs.evening_detective_server.DeleteTeamReq - 116, // 102: crabs.evening_detective_server.EveningDetectiveServer.GiveTeamApplications:input_type -> crabs.evening_detective_server.GiveTeamApplicationsReq - 118, // 103: crabs.evening_detective_server.EveningDetectiveServer.GetTeamStory:input_type -> crabs.evening_detective_server.GetTeamStoryReq - 121, // 104: crabs.evening_detective_server.EveningDetectiveServer.AddTeamAction:input_type -> crabs.evening_detective_server.AddTeamActionReq - 123, // 105: crabs.evening_detective_server.EveningDetectiveServer.DeleteLastTeamAction:input_type -> crabs.evening_detective_server.DeleteLastTeamActionReq - 125, // 106: crabs.evening_detective_server.EveningDetectiveServer.GetTeamAnswers:input_type -> crabs.evening_detective_server.GetTeamAnswersReq - 127, // 107: crabs.evening_detective_server.EveningDetectiveServer.SubmitTeamAnswers:input_type -> crabs.evening_detective_server.SubmitTeamAnswersReq - 129, // 108: crabs.evening_detective_server.EveningDetectiveServer.SetTeamAnswerScore:input_type -> crabs.evening_detective_server.SetTeamAnswerScoreReq - 1, // 109: crabs.evening_detective_server.EveningDetectiveServer.Ping:output_type -> crabs.evening_detective_server.PingRsp - 3, // 110: crabs.evening_detective_server.EveningDetectiveServer.Echo:output_type -> crabs.evening_detective_server.EchoRsp - 5, // 111: crabs.evening_detective_server.EveningDetectiveServer.Signup:output_type -> crabs.evening_detective_server.SignupRsp - 7, // 112: crabs.evening_detective_server.EveningDetectiveServer.RefreshPassword:output_type -> crabs.evening_detective_server.RefreshPasswordRsp - 9, // 113: crabs.evening_detective_server.EveningDetectiveServer.Login:output_type -> crabs.evening_detective_server.LoginRsp - 11, // 114: crabs.evening_detective_server.EveningDetectiveServer.Refresh:output_type -> crabs.evening_detective_server.RefreshRsp - 13, // 115: crabs.evening_detective_server.EveningDetectiveServer.DeleteAccount:output_type -> crabs.evening_detective_server.DeleteAccountRsp - 15, // 116: crabs.evening_detective_server.EveningDetectiveServer.GetTerms:output_type -> crabs.evening_detective_server.GetTermsRsp - 17, // 117: crabs.evening_detective_server.EveningDetectiveServer.GetPrivacy:output_type -> crabs.evening_detective_server.GetPrivacyRsp - 19, // 118: crabs.evening_detective_server.EveningDetectiveServer.GetConsent:output_type -> crabs.evening_detective_server.GetConsentRsp - 21, // 119: crabs.evening_detective_server.EveningDetectiveServer.GetUsers:output_type -> crabs.evening_detective_server.GetUsersRsp - 24, // 120: crabs.evening_detective_server.EveningDetectiveServer.GetUserById:output_type -> crabs.evening_detective_server.GetUserByIdRsp - 26, // 121: crabs.evening_detective_server.EveningDetectiveServer.GetMe:output_type -> crabs.evening_detective_server.GetMeRsp - 28, // 122: crabs.evening_detective_server.EveningDetectiveServer.AddUserRole:output_type -> crabs.evening_detective_server.AddUserRoleRsp - 30, // 123: crabs.evening_detective_server.EveningDetectiveServer.DeleteUserRole:output_type -> crabs.evening_detective_server.DeleteUserRoleRsp - 32, // 124: crabs.evening_detective_server.EveningDetectiveServer.GetPermissions:output_type -> crabs.evening_detective_server.GetPermissionsRsp - 34, // 125: crabs.evening_detective_server.EveningDetectiveServer.UploadFile:output_type -> crabs.evening_detective_server.UploadFileRsp - 134, // 126: crabs.evening_detective_server.EveningDetectiveServer.DownloadFile:output_type -> google.api.HttpBody - 37, // 127: crabs.evening_detective_server.EveningDetectiveServer.AddScenario:output_type -> crabs.evening_detective_server.AddScenarioRsp - 39, // 128: crabs.evening_detective_server.EveningDetectiveServer.GetMyScenarios:output_type -> crabs.evening_detective_server.GetMyScenariosRsp - 41, // 129: crabs.evening_detective_server.EveningDetectiveServer.GetScenariosCatalog:output_type -> crabs.evening_detective_server.GetScenariosCatalogRsp - 43, // 130: crabs.evening_detective_server.EveningDetectiveServer.GetFullScenario:output_type -> crabs.evening_detective_server.GetScenarioRsp - 43, // 131: crabs.evening_detective_server.EveningDetectiveServer.GetScenario:output_type -> crabs.evening_detective_server.GetScenarioRsp - 57, // 132: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenario:output_type -> crabs.evening_detective_server.UpdateScenarioRsp - 59, // 133: crabs.evening_detective_server.EveningDetectiveServer.PublicScenario:output_type -> crabs.evening_detective_server.PublicScenarioRsp - 61, // 134: crabs.evening_detective_server.EveningDetectiveServer.DraftScenario:output_type -> crabs.evening_detective_server.DraftScenarioRsp - 63, // 135: crabs.evening_detective_server.EveningDetectiveServer.DeleteScenario:output_type -> crabs.evening_detective_server.DeleteScenarioRsp - 65, // 136: crabs.evening_detective_server.EveningDetectiveServer.AddScenarioPlace:output_type -> crabs.evening_detective_server.AddScenarioPlaceRsp - 67, // 137: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioPlace:output_type -> crabs.evening_detective_server.UpdateScenarioPlaceRsp - 69, // 138: crabs.evening_detective_server.EveningDetectiveServer.DeleteScenarioPlace:output_type -> crabs.evening_detective_server.DeleteScenarioPlaceRsp - 75, // 139: crabs.evening_detective_server.EveningDetectiveServer.AddScenarioQuestion:output_type -> crabs.evening_detective_server.AddScenarioQuestionRsp - 77, // 140: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioQuestion:output_type -> crabs.evening_detective_server.UpdateScenarioQuestionRsp - 79, // 141: crabs.evening_detective_server.EveningDetectiveServer.DeleteScenarioQuestion:output_type -> crabs.evening_detective_server.DeleteScenarioQuestionRsp - 132, // 142: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioBackground:output_type -> crabs.evening_detective_server.UpdateScenarioBackgroundRsp - 73, // 143: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioIntro:output_type -> crabs.evening_detective_server.UpdateScenarioIntroRsp - 71, // 144: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioConclusion:output_type -> crabs.evening_detective_server.UpdateScenarioConclusionRsp - 134, // 145: crabs.evening_detective_server.EveningDetectiveServer.DownloadScenarioArchive:output_type -> google.api.HttpBody - 81, // 146: crabs.evening_detective_server.EveningDetectiveServer.UploadScenarioArchive:output_type -> crabs.evening_detective_server.UploadScenarioArchiveRsp - 83, // 147: crabs.evening_detective_server.EveningDetectiveServer.AddGame:output_type -> crabs.evening_detective_server.AddGameRsp - 85, // 148: crabs.evening_detective_server.EveningDetectiveServer.GetGames:output_type -> crabs.evening_detective_server.GetGamesRsp - 89, // 149: crabs.evening_detective_server.EveningDetectiveServer.GetGame:output_type -> crabs.evening_detective_server.GetGameRsp - 91, // 150: crabs.evening_detective_server.EveningDetectiveServer.GetGameAnswers:output_type -> crabs.evening_detective_server.GetGameAnswersRsp - 95, // 151: crabs.evening_detective_server.EveningDetectiveServer.GetGameResults:output_type -> crabs.evening_detective_server.GetGameResultsRsp - 97, // 152: crabs.evening_detective_server.EveningDetectiveServer.UpdateGame:output_type -> crabs.evening_detective_server.UpdateGameRsp - 99, // 153: crabs.evening_detective_server.EveningDetectiveServer.StartGame:output_type -> crabs.evening_detective_server.StartGameRsp - 101, // 154: crabs.evening_detective_server.EveningDetectiveServer.PauseGame:output_type -> crabs.evening_detective_server.PauseGameRsp - 103, // 155: crabs.evening_detective_server.EveningDetectiveServer.PlayGame:output_type -> crabs.evening_detective_server.PlayGameRsp - 105, // 156: crabs.evening_detective_server.EveningDetectiveServer.FinishGame:output_type -> crabs.evening_detective_server.FinishGameRsp - 107, // 157: crabs.evening_detective_server.EveningDetectiveServer.ResetGame:output_type -> crabs.evening_detective_server.ResetGameRsp - 109, // 158: crabs.evening_detective_server.EveningDetectiveServer.DeleteGame:output_type -> crabs.evening_detective_server.DeleteGameRsp - 111, // 159: crabs.evening_detective_server.EveningDetectiveServer.AddTeam:output_type -> crabs.evening_detective_server.AddTeamRsp - 113, // 160: crabs.evening_detective_server.EveningDetectiveServer.UpdateTeam:output_type -> crabs.evening_detective_server.UpdateTeamRsp - 115, // 161: crabs.evening_detective_server.EveningDetectiveServer.DeleteTeam:output_type -> crabs.evening_detective_server.DeleteTeamRsp - 117, // 162: crabs.evening_detective_server.EveningDetectiveServer.GiveTeamApplications:output_type -> crabs.evening_detective_server.GiveTeamApplicationsRsp - 119, // 163: crabs.evening_detective_server.EveningDetectiveServer.GetTeamStory:output_type -> crabs.evening_detective_server.GetTeamStoryRsp - 122, // 164: crabs.evening_detective_server.EveningDetectiveServer.AddTeamAction:output_type -> crabs.evening_detective_server.AddTeamActionRsp - 124, // 165: crabs.evening_detective_server.EveningDetectiveServer.DeleteLastTeamAction:output_type -> crabs.evening_detective_server.DeleteLastTeamActionRsp - 126, // 166: crabs.evening_detective_server.EveningDetectiveServer.GetTeamAnswers:output_type -> crabs.evening_detective_server.GetTeamAnswersRsp - 128, // 167: crabs.evening_detective_server.EveningDetectiveServer.SubmitTeamAnswers:output_type -> crabs.evening_detective_server.SubmitTeamAnswersRsp - 130, // 168: crabs.evening_detective_server.EveningDetectiveServer.SetTeamAnswerScore:output_type -> crabs.evening_detective_server.SetTeamAnswerScoreRsp - 109, // [109:169] is the sub-list for method output_type - 49, // [49:109] is the sub-list for method input_type + 88, // 90: crabs.evening_detective_server.EveningDetectiveServer.DownloadTeamQR:input_type -> crabs.evening_detective_server.GetGameReq + 90, // 91: crabs.evening_detective_server.EveningDetectiveServer.GetGameAnswers:input_type -> crabs.evening_detective_server.GetGameAnswersReq + 92, // 92: crabs.evening_detective_server.EveningDetectiveServer.GetGameResults:input_type -> crabs.evening_detective_server.GetGameResultsReq + 96, // 93: crabs.evening_detective_server.EveningDetectiveServer.UpdateGame:input_type -> crabs.evening_detective_server.UpdateGameReq + 98, // 94: crabs.evening_detective_server.EveningDetectiveServer.StartGame:input_type -> crabs.evening_detective_server.StartGameReq + 100, // 95: crabs.evening_detective_server.EveningDetectiveServer.PauseGame:input_type -> crabs.evening_detective_server.PauseGameReq + 102, // 96: crabs.evening_detective_server.EveningDetectiveServer.PlayGame:input_type -> crabs.evening_detective_server.PlayGameReq + 104, // 97: crabs.evening_detective_server.EveningDetectiveServer.FinishGame:input_type -> crabs.evening_detective_server.FinishGameReq + 106, // 98: crabs.evening_detective_server.EveningDetectiveServer.ResetGame:input_type -> crabs.evening_detective_server.ResetGameReq + 108, // 99: crabs.evening_detective_server.EveningDetectiveServer.DeleteGame:input_type -> crabs.evening_detective_server.DeleteGameReq + 110, // 100: crabs.evening_detective_server.EveningDetectiveServer.AddTeam:input_type -> crabs.evening_detective_server.AddTeamReq + 112, // 101: crabs.evening_detective_server.EveningDetectiveServer.UpdateTeam:input_type -> crabs.evening_detective_server.UpdateTeamReq + 114, // 102: crabs.evening_detective_server.EveningDetectiveServer.DeleteTeam:input_type -> crabs.evening_detective_server.DeleteTeamReq + 116, // 103: crabs.evening_detective_server.EveningDetectiveServer.GiveTeamApplications:input_type -> crabs.evening_detective_server.GiveTeamApplicationsReq + 118, // 104: crabs.evening_detective_server.EveningDetectiveServer.GetTeamStory:input_type -> crabs.evening_detective_server.GetTeamStoryReq + 121, // 105: crabs.evening_detective_server.EveningDetectiveServer.AddTeamAction:input_type -> crabs.evening_detective_server.AddTeamActionReq + 123, // 106: crabs.evening_detective_server.EveningDetectiveServer.DeleteLastTeamAction:input_type -> crabs.evening_detective_server.DeleteLastTeamActionReq + 125, // 107: crabs.evening_detective_server.EveningDetectiveServer.GetTeamAnswers:input_type -> crabs.evening_detective_server.GetTeamAnswersReq + 127, // 108: crabs.evening_detective_server.EveningDetectiveServer.SubmitTeamAnswers:input_type -> crabs.evening_detective_server.SubmitTeamAnswersReq + 129, // 109: crabs.evening_detective_server.EveningDetectiveServer.SetTeamAnswerScore:input_type -> crabs.evening_detective_server.SetTeamAnswerScoreReq + 1, // 110: crabs.evening_detective_server.EveningDetectiveServer.Ping:output_type -> crabs.evening_detective_server.PingRsp + 3, // 111: crabs.evening_detective_server.EveningDetectiveServer.Echo:output_type -> crabs.evening_detective_server.EchoRsp + 5, // 112: crabs.evening_detective_server.EveningDetectiveServer.Signup:output_type -> crabs.evening_detective_server.SignupRsp + 7, // 113: crabs.evening_detective_server.EveningDetectiveServer.RefreshPassword:output_type -> crabs.evening_detective_server.RefreshPasswordRsp + 9, // 114: crabs.evening_detective_server.EveningDetectiveServer.Login:output_type -> crabs.evening_detective_server.LoginRsp + 11, // 115: crabs.evening_detective_server.EveningDetectiveServer.Refresh:output_type -> crabs.evening_detective_server.RefreshRsp + 13, // 116: crabs.evening_detective_server.EveningDetectiveServer.DeleteAccount:output_type -> crabs.evening_detective_server.DeleteAccountRsp + 15, // 117: crabs.evening_detective_server.EveningDetectiveServer.GetTerms:output_type -> crabs.evening_detective_server.GetTermsRsp + 17, // 118: crabs.evening_detective_server.EveningDetectiveServer.GetPrivacy:output_type -> crabs.evening_detective_server.GetPrivacyRsp + 19, // 119: crabs.evening_detective_server.EveningDetectiveServer.GetConsent:output_type -> crabs.evening_detective_server.GetConsentRsp + 21, // 120: crabs.evening_detective_server.EveningDetectiveServer.GetUsers:output_type -> crabs.evening_detective_server.GetUsersRsp + 24, // 121: crabs.evening_detective_server.EveningDetectiveServer.GetUserById:output_type -> crabs.evening_detective_server.GetUserByIdRsp + 26, // 122: crabs.evening_detective_server.EveningDetectiveServer.GetMe:output_type -> crabs.evening_detective_server.GetMeRsp + 28, // 123: crabs.evening_detective_server.EveningDetectiveServer.AddUserRole:output_type -> crabs.evening_detective_server.AddUserRoleRsp + 30, // 124: crabs.evening_detective_server.EveningDetectiveServer.DeleteUserRole:output_type -> crabs.evening_detective_server.DeleteUserRoleRsp + 32, // 125: crabs.evening_detective_server.EveningDetectiveServer.GetPermissions:output_type -> crabs.evening_detective_server.GetPermissionsRsp + 34, // 126: crabs.evening_detective_server.EveningDetectiveServer.UploadFile:output_type -> crabs.evening_detective_server.UploadFileRsp + 134, // 127: crabs.evening_detective_server.EveningDetectiveServer.DownloadFile:output_type -> google.api.HttpBody + 37, // 128: crabs.evening_detective_server.EveningDetectiveServer.AddScenario:output_type -> crabs.evening_detective_server.AddScenarioRsp + 39, // 129: crabs.evening_detective_server.EveningDetectiveServer.GetMyScenarios:output_type -> crabs.evening_detective_server.GetMyScenariosRsp + 41, // 130: crabs.evening_detective_server.EveningDetectiveServer.GetScenariosCatalog:output_type -> crabs.evening_detective_server.GetScenariosCatalogRsp + 43, // 131: crabs.evening_detective_server.EveningDetectiveServer.GetFullScenario:output_type -> crabs.evening_detective_server.GetScenarioRsp + 43, // 132: crabs.evening_detective_server.EveningDetectiveServer.GetScenario:output_type -> crabs.evening_detective_server.GetScenarioRsp + 57, // 133: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenario:output_type -> crabs.evening_detective_server.UpdateScenarioRsp + 59, // 134: crabs.evening_detective_server.EveningDetectiveServer.PublicScenario:output_type -> crabs.evening_detective_server.PublicScenarioRsp + 61, // 135: crabs.evening_detective_server.EveningDetectiveServer.DraftScenario:output_type -> crabs.evening_detective_server.DraftScenarioRsp + 63, // 136: crabs.evening_detective_server.EveningDetectiveServer.DeleteScenario:output_type -> crabs.evening_detective_server.DeleteScenarioRsp + 65, // 137: crabs.evening_detective_server.EveningDetectiveServer.AddScenarioPlace:output_type -> crabs.evening_detective_server.AddScenarioPlaceRsp + 67, // 138: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioPlace:output_type -> crabs.evening_detective_server.UpdateScenarioPlaceRsp + 69, // 139: crabs.evening_detective_server.EveningDetectiveServer.DeleteScenarioPlace:output_type -> crabs.evening_detective_server.DeleteScenarioPlaceRsp + 75, // 140: crabs.evening_detective_server.EveningDetectiveServer.AddScenarioQuestion:output_type -> crabs.evening_detective_server.AddScenarioQuestionRsp + 77, // 141: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioQuestion:output_type -> crabs.evening_detective_server.UpdateScenarioQuestionRsp + 79, // 142: crabs.evening_detective_server.EveningDetectiveServer.DeleteScenarioQuestion:output_type -> crabs.evening_detective_server.DeleteScenarioQuestionRsp + 132, // 143: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioBackground:output_type -> crabs.evening_detective_server.UpdateScenarioBackgroundRsp + 73, // 144: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioIntro:output_type -> crabs.evening_detective_server.UpdateScenarioIntroRsp + 71, // 145: crabs.evening_detective_server.EveningDetectiveServer.UpdateScenarioConclusion:output_type -> crabs.evening_detective_server.UpdateScenarioConclusionRsp + 134, // 146: crabs.evening_detective_server.EveningDetectiveServer.DownloadScenarioArchive:output_type -> google.api.HttpBody + 81, // 147: crabs.evening_detective_server.EveningDetectiveServer.UploadScenarioArchive:output_type -> crabs.evening_detective_server.UploadScenarioArchiveRsp + 83, // 148: crabs.evening_detective_server.EveningDetectiveServer.AddGame:output_type -> crabs.evening_detective_server.AddGameRsp + 85, // 149: crabs.evening_detective_server.EveningDetectiveServer.GetGames:output_type -> crabs.evening_detective_server.GetGamesRsp + 89, // 150: crabs.evening_detective_server.EveningDetectiveServer.GetGame:output_type -> crabs.evening_detective_server.GetGameRsp + 134, // 151: crabs.evening_detective_server.EveningDetectiveServer.DownloadTeamQR:output_type -> google.api.HttpBody + 91, // 152: crabs.evening_detective_server.EveningDetectiveServer.GetGameAnswers:output_type -> crabs.evening_detective_server.GetGameAnswersRsp + 95, // 153: crabs.evening_detective_server.EveningDetectiveServer.GetGameResults:output_type -> crabs.evening_detective_server.GetGameResultsRsp + 97, // 154: crabs.evening_detective_server.EveningDetectiveServer.UpdateGame:output_type -> crabs.evening_detective_server.UpdateGameRsp + 99, // 155: crabs.evening_detective_server.EveningDetectiveServer.StartGame:output_type -> crabs.evening_detective_server.StartGameRsp + 101, // 156: crabs.evening_detective_server.EveningDetectiveServer.PauseGame:output_type -> crabs.evening_detective_server.PauseGameRsp + 103, // 157: crabs.evening_detective_server.EveningDetectiveServer.PlayGame:output_type -> crabs.evening_detective_server.PlayGameRsp + 105, // 158: crabs.evening_detective_server.EveningDetectiveServer.FinishGame:output_type -> crabs.evening_detective_server.FinishGameRsp + 107, // 159: crabs.evening_detective_server.EveningDetectiveServer.ResetGame:output_type -> crabs.evening_detective_server.ResetGameRsp + 109, // 160: crabs.evening_detective_server.EveningDetectiveServer.DeleteGame:output_type -> crabs.evening_detective_server.DeleteGameRsp + 111, // 161: crabs.evening_detective_server.EveningDetectiveServer.AddTeam:output_type -> crabs.evening_detective_server.AddTeamRsp + 113, // 162: crabs.evening_detective_server.EveningDetectiveServer.UpdateTeam:output_type -> crabs.evening_detective_server.UpdateTeamRsp + 115, // 163: crabs.evening_detective_server.EveningDetectiveServer.DeleteTeam:output_type -> crabs.evening_detective_server.DeleteTeamRsp + 117, // 164: crabs.evening_detective_server.EveningDetectiveServer.GiveTeamApplications:output_type -> crabs.evening_detective_server.GiveTeamApplicationsRsp + 119, // 165: crabs.evening_detective_server.EveningDetectiveServer.GetTeamStory:output_type -> crabs.evening_detective_server.GetTeamStoryRsp + 122, // 166: crabs.evening_detective_server.EveningDetectiveServer.AddTeamAction:output_type -> crabs.evening_detective_server.AddTeamActionRsp + 124, // 167: crabs.evening_detective_server.EveningDetectiveServer.DeleteLastTeamAction:output_type -> crabs.evening_detective_server.DeleteLastTeamActionRsp + 126, // 168: crabs.evening_detective_server.EveningDetectiveServer.GetTeamAnswers:output_type -> crabs.evening_detective_server.GetTeamAnswersRsp + 128, // 169: crabs.evening_detective_server.EveningDetectiveServer.SubmitTeamAnswers:output_type -> crabs.evening_detective_server.SubmitTeamAnswersRsp + 130, // 170: crabs.evening_detective_server.EveningDetectiveServer.SetTeamAnswerScore:output_type -> crabs.evening_detective_server.SetTeamAnswerScoreRsp + 110, // [110:171] is the sub-list for method output_type + 49, // [49:110] is the sub-list for method input_type 49, // [49:49] is the sub-list for extension type_name 49, // [49:49] is the sub-list for extension extendee 0, // [0:49] is the sub-list for field type_name diff --git a/proto/main.pb.gw.go b/proto/main.pb.gw.go index 58abf0d..da0cc06 100644 --- a/proto/main.pb.gw.go +++ b/proto/main.pb.gw.go @@ -1473,6 +1473,45 @@ func local_request_EveningDetectiveServer_GetGame_0(ctx context.Context, marshal return msg, metadata, err } +func request_EveningDetectiveServer_DownloadTeamQR_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetGameReq + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + protoReq.Id, err = runtime.Int32(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + msg, err := client.DownloadTeamQR(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_EveningDetectiveServer_DownloadTeamQR_0(ctx context.Context, marshaler runtime.Marshaler, server EveningDetectiveServerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetGameReq + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + protoReq.Id, err = runtime.Int32(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + msg, err := server.DownloadTeamQR(ctx, &protoReq) + return msg, metadata, err +} + func request_EveningDetectiveServer_GetGameAnswers_0(ctx context.Context, marshaler runtime.Marshaler, client EveningDetectiveServerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var ( protoReq GetGameAnswersReq @@ -3138,6 +3177,26 @@ func RegisterEveningDetectiveServerHandlerServer(ctx context.Context, mux *runti } forward_EveningDetectiveServer_GetGame_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_DownloadTeamQR_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/DownloadTeamQR", runtime.WithHTTPPathPattern("/api/games/{id}/teams/qr.pdf")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_EveningDetectiveServer_DownloadTeamQR_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_DownloadTeamQR_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetGameAnswers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -4255,6 +4314,23 @@ func RegisterEveningDetectiveServerHandlerClient(ctx context.Context, mux *runti } forward_EveningDetectiveServer_GetGame_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) + mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_DownloadTeamQR_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/DownloadTeamQR", runtime.WithHTTPPathPattern("/api/games/{id}/teams/qr.pdf")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_EveningDetectiveServer_DownloadTeamQR_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_DownloadTeamQR_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) mux.Handle(http.MethodGet, pattern_EveningDetectiveServer_GetGameAnswers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -4623,6 +4699,7 @@ var ( pattern_EveningDetectiveServer_AddGame_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "games"}, "")) pattern_EveningDetectiveServer_GetGames_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1}, []string{"api", "games"}, "")) pattern_EveningDetectiveServer_GetGame_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "games", "id"}, "")) + pattern_EveningDetectiveServer_DownloadTeamQR_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3, 2, 4}, []string{"api", "games", "id", "teams", "qr.pdf"}, "")) pattern_EveningDetectiveServer_GetGameAnswers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "gameId", "answers"}, "")) pattern_EveningDetectiveServer_GetGameResults_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2, 2, 3}, []string{"api", "games", "gameId", "results"}, "")) pattern_EveningDetectiveServer_UpdateGame_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"api", "games", "id"}, "")) @@ -4686,6 +4763,7 @@ var ( forward_EveningDetectiveServer_AddGame_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_GetGames_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_GetGame_0 = runtime.ForwardResponseMessage + forward_EveningDetectiveServer_DownloadTeamQR_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_GetGameAnswers_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_GetGameResults_0 = runtime.ForwardResponseMessage forward_EveningDetectiveServer_UpdateGame_0 = runtime.ForwardResponseMessage diff --git a/proto/main_grpc.pb.go b/proto/main_grpc.pb.go index 0faa22c..287008a 100644 --- a/proto/main_grpc.pb.go +++ b/proto/main_grpc.pb.go @@ -61,6 +61,7 @@ const ( EveningDetectiveServer_AddGame_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/AddGame" EveningDetectiveServer_GetGames_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetGames" EveningDetectiveServer_GetGame_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetGame" + EveningDetectiveServer_DownloadTeamQR_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/DownloadTeamQR" EveningDetectiveServer_GetGameAnswers_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetGameAnswers" EveningDetectiveServer_GetGameResults_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/GetGameResults" EveningDetectiveServer_UpdateGame_FullMethodName = "/crabs.evening_detective_server.EveningDetectiveServer/UpdateGame" @@ -129,6 +130,7 @@ type EveningDetectiveServerClient interface { AddGame(ctx context.Context, in *AddGameReq, opts ...grpc.CallOption) (*AddGameRsp, error) GetGames(ctx context.Context, in *GetGamesReq, opts ...grpc.CallOption) (*GetGamesRsp, error) GetGame(ctx context.Context, in *GetGameReq, opts ...grpc.CallOption) (*GetGameRsp, error) + DownloadTeamQR(ctx context.Context, in *GetGameReq, opts ...grpc.CallOption) (*httpbody.HttpBody, error) GetGameAnswers(ctx context.Context, in *GetGameAnswersReq, opts ...grpc.CallOption) (*GetGameAnswersRsp, error) GetGameResults(ctx context.Context, in *GetGameResultsReq, opts ...grpc.CallOption) (*GetGameResultsRsp, error) UpdateGame(ctx context.Context, in *UpdateGameReq, opts ...grpc.CallOption) (*UpdateGameRsp, error) @@ -568,6 +570,16 @@ func (c *eveningDetectiveServerClient) GetGame(ctx context.Context, in *GetGameR return out, nil } +func (c *eveningDetectiveServerClient) DownloadTeamQR(ctx context.Context, in *GetGameReq, opts ...grpc.CallOption) (*httpbody.HttpBody, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(httpbody.HttpBody) + err := c.cc.Invoke(ctx, EveningDetectiveServer_DownloadTeamQR_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *eveningDetectiveServerClient) GetGameAnswers(ctx context.Context, in *GetGameAnswersReq, opts ...grpc.CallOption) (*GetGameAnswersRsp, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GetGameAnswersRsp) @@ -805,6 +817,7 @@ type EveningDetectiveServerServer interface { AddGame(context.Context, *AddGameReq) (*AddGameRsp, error) GetGames(context.Context, *GetGamesReq) (*GetGamesRsp, error) GetGame(context.Context, *GetGameReq) (*GetGameRsp, error) + DownloadTeamQR(context.Context, *GetGameReq) (*httpbody.HttpBody, error) GetGameAnswers(context.Context, *GetGameAnswersReq) (*GetGameAnswersRsp, error) GetGameResults(context.Context, *GetGameResultsReq) (*GetGameResultsRsp, error) UpdateGame(context.Context, *UpdateGameReq) (*UpdateGameRsp, error) @@ -957,6 +970,9 @@ func (UnimplementedEveningDetectiveServerServer) GetGames(context.Context, *GetG func (UnimplementedEveningDetectiveServerServer) GetGame(context.Context, *GetGameReq) (*GetGameRsp, error) { return nil, status.Error(codes.Unimplemented, "method GetGame not implemented") } +func (UnimplementedEveningDetectiveServerServer) DownloadTeamQR(context.Context, *GetGameReq) (*httpbody.HttpBody, error) { + return nil, status.Error(codes.Unimplemented, "method DownloadTeamQR not implemented") +} func (UnimplementedEveningDetectiveServerServer) GetGameAnswers(context.Context, *GetGameAnswersReq) (*GetGameAnswersRsp, error) { return nil, status.Error(codes.Unimplemented, "method GetGameAnswers not implemented") } @@ -1774,6 +1790,24 @@ func _EveningDetectiveServer_GetGame_Handler(srv interface{}, ctx context.Contex return interceptor(ctx, in, info, handler) } +func _EveningDetectiveServer_DownloadTeamQR_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetGameReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(EveningDetectiveServerServer).DownloadTeamQR(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: EveningDetectiveServer_DownloadTeamQR_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EveningDetectiveServerServer).DownloadTeamQR(ctx, req.(*GetGameReq)) + } + return interceptor(ctx, in, info, handler) +} + func _EveningDetectiveServer_GetGameAnswers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(GetGameAnswersReq) if err := dec(in); err != nil { @@ -2287,6 +2321,10 @@ var EveningDetectiveServer_ServiceDesc = grpc.ServiceDesc{ MethodName: "GetGame", Handler: _EveningDetectiveServer_GetGame_Handler, }, + { + MethodName: "DownloadTeamQR", + Handler: _EveningDetectiveServer_DownloadTeamQR_Handler, + }, { MethodName: "GetGameAnswers", Handler: _EveningDetectiveServer_GetGameAnswers_Handler,