generated from VLADIMIR/template
add qr-s
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
@@ -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$
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user