generated from VLADIMIR/template
update
This commit is contained in:
@@ -3,14 +3,14 @@ package app
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"evening_detective_server/internal/repos/teams_repo"
|
"evening_detective_server/internal/services/participation_service"
|
||||||
"evening_detective_server/proto"
|
"evening_detective_server/proto"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/metadata"
|
"google.golang.org/grpc/metadata"
|
||||||
"google.golang.org/protobuf/types/known/timestamppb"
|
"google.golang.org/protobuf/types/known/timestamppb"
|
||||||
)
|
)
|
||||||
|
|
||||||
func mapParticipationResponse(p *teams_repo.Participation) *proto.ParticipationRsp {
|
func mapParticipationResponse(p *participation_service.Participation) *proto.ParticipationRsp {
|
||||||
result := &proto.ParticipationRsp{}
|
result := &proto.ParticipationRsp{}
|
||||||
if p != nil {
|
if p != nil {
|
||||||
result.TeamName = p.TeamName
|
result.TeamName = p.TeamName
|
||||||
@@ -24,7 +24,7 @@ func mapParticipationResponse(p *teams_repo.Participation) *proto.ParticipationR
|
|||||||
}
|
}
|
||||||
|
|
||||||
func participationError(err error) string {
|
func participationError(err error) string {
|
||||||
if errors.Is(err, teams_repo.ErrParticipationLink) || errors.Is(err, teams_repo.ErrParticipationFinal) || errors.Is(err, teams_repo.ErrParticipationClosed) {
|
if errors.Is(err, participation_service.ErrParticipationLink) || errors.Is(err, participation_service.ErrParticipationFinal) || errors.Is(err, participation_service.ErrParticipationClosed) {
|
||||||
return err.Error()
|
return err.Error()
|
||||||
}
|
}
|
||||||
return "Не удалось обработать ответ. Попробуйте позже."
|
return "Не удалось обработать ответ. Попробуйте позже."
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package participation_service
|
||||||
|
|
||||||
|
import "evening_detective_server/internal/repos/teams_repo"
|
||||||
|
|
||||||
|
func mapParticipation(p *teams_repo.Participation) *Participation {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &Participation{
|
||||||
|
TeamID: p.TeamID,
|
||||||
|
TeamName: p.TeamName,
|
||||||
|
GameName: p.GameName,
|
||||||
|
StartAt: p.StartAt,
|
||||||
|
ParticipantsCount: p.ParticipantsCount,
|
||||||
|
Status: p.Status,
|
||||||
|
Closed: p.Closed,
|
||||||
|
Email: p.Email,
|
||||||
|
Attempts: p.Attempts,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package participation_service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"evening_detective_server/internal/repos/teams_repo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Participation — данные участия, доступные транспортному и email-сценариям.
|
||||||
|
type Participation struct {
|
||||||
|
TeamID int
|
||||||
|
TeamName string
|
||||||
|
GameName string
|
||||||
|
StartAt time.Time
|
||||||
|
ParticipantsCount int32
|
||||||
|
Status string
|
||||||
|
Closed bool
|
||||||
|
Email string
|
||||||
|
Attempts int
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrParticipationLink = teams_repo.ErrParticipationLink
|
||||||
|
ErrParticipationClosed = teams_repo.ErrParticipationClosed
|
||||||
|
ErrParticipationFinal = teams_repo.ErrParticipationFinal
|
||||||
|
)
|
||||||
@@ -33,7 +33,7 @@ func TestParticipationEmail(t *testing.T) {
|
|||||||
if len(token) != 64 || token != participationToken("test-secret", 42) || token == participationToken("test-secret", 43) || token == participationToken("other", 42) {
|
if len(token) != 64 || token != participationToken("test-secret", 42) || token == participationToken("test-secret", 43) || token == participationToken("other", 42) {
|
||||||
t.Fatal("token isolation or retry stability broken")
|
t.Fatal("token isolation or retry stability broken")
|
||||||
}
|
}
|
||||||
p := &teams_repo.Participation{TeamName: "<script>team</script>", GameName: "Game", Email: "captain@example.org", StartAt: time.Date(2026, 9, 20, 12, 0, 0, 0, time.UTC), ParticipantsCount: 5}
|
p := &Participation{TeamName: "<script>team</script>", GameName: "Game", Email: "captain@example.org", StartAt: time.Date(2026, 9, 20, 12, 0, 0, 0, time.UTC), ParticipantsCount: 5}
|
||||||
m := participationEmail(p, "https://example.org", token, time.FixedZone("+07", 7*3600))
|
m := participationEmail(p, "https://example.org", token, time.FixedZone("+07", 7*3600))
|
||||||
if strings.Contains(m.HTML, "<script>") || !strings.Contains(m.HTML, "<script>") || !strings.Contains(m.Body, "19:00") || !strings.Contains(m.Body, "#token="+token) || !strings.Contains(m.HTML, "answer=cancelled") {
|
if strings.Contains(m.HTML, "<script>") || !strings.Contains(m.HTML, "<script>") || !strings.Contains(m.Body, "19:00") || !strings.Contains(m.Body, "#token="+token) || !strings.Contains(m.HTML, "answer=cancelled") {
|
||||||
t.Fatalf("invalid email: %+v", m)
|
t.Fatalf("invalid email: %+v", m)
|
||||||
@@ -174,10 +174,10 @@ func TestParticipationPostgres(t *testing.T) {
|
|||||||
t.Fatal("email retry exceeded the eight-attempt limit")
|
t.Fatal("email retry exceeded the eight-attempt limit")
|
||||||
}
|
}
|
||||||
token := participationToken("secret", 1)
|
token := participationToken("secret", 1)
|
||||||
if _, err := service.GetParticipation(ctx, "bad"); !errors.Is(err, teams_repo.ErrParticipationLink) {
|
if _, err := service.GetParticipation(ctx, "bad"); !errors.Is(err, ErrParticipationLink) {
|
||||||
t.Fatalf("invalid token: %v", err)
|
t.Fatalf("invalid token: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := service.GetParticipation(ctx, strings.Repeat("0", 64)); !errors.Is(err, teams_repo.ErrParticipationLink) {
|
if _, err := service.GetParticipation(ctx, strings.Repeat("0", 64)); !errors.Is(err, ErrParticipationLink) {
|
||||||
t.Fatalf("unknown token: %v", err)
|
t.Fatalf("unknown token: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := service.GetParticipation(ctx, token); err != nil {
|
if _, err := service.GetParticipation(ctx, token); err != nil {
|
||||||
@@ -193,7 +193,7 @@ func TestParticipationPostgres(t *testing.T) {
|
|||||||
err := <-answers
|
err := <-answers
|
||||||
if err == nil {
|
if err == nil {
|
||||||
success++
|
success++
|
||||||
} else if errors.Is(err, teams_repo.ErrParticipationFinal) {
|
} else if errors.Is(err, ErrParticipationFinal) {
|
||||||
conflict++
|
conflict++
|
||||||
} else {
|
} else {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -239,11 +239,11 @@ func TestParticipationPostgres(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
exec(`UPDATE games SET status='run' WHERE id=1`)
|
exec(`UPDATE games SET status='run' WHERE id=1`)
|
||||||
if _, err := service.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, teams_repo.ErrParticipationClosed) {
|
if _, err := service.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, ErrParticipationClosed) {
|
||||||
t.Fatalf("running game accepted answer: %v", err)
|
t.Fatalf("running game accepted answer: %v", err)
|
||||||
}
|
}
|
||||||
exec(`UPDATE games SET status='draft',start_at=(NOW() AT TIME ZONE 'UTC')-INTERVAL '1 second' WHERE id=1`)
|
exec(`UPDATE games SET status='draft',start_at=(NOW() AT TIME ZONE 'UTC')-INTERVAL '1 second' WHERE id=1`)
|
||||||
if _, err := service.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, teams_repo.ErrParticipationClosed) {
|
if _, err := service.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, ErrParticipationClosed) {
|
||||||
t.Fatalf("past scheduled start accepted answer: %v", err)
|
t.Fatalf("past scheduled start accepted answer: %v", err)
|
||||||
}
|
}
|
||||||
state(6, "pending")
|
state(6, "pending")
|
||||||
|
|||||||
@@ -28,11 +28,13 @@ func NewParticipationService(teamsRepo *teams_repo.TeamsRepo, appBaseURL string)
|
|||||||
return &ParticipationService{teamsRepo: teamsRepo, appBaseURL: appBaseURL}
|
return &ParticipationService{teamsRepo: teamsRepo, appBaseURL: appBaseURL}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ParticipationService) GetParticipation(ctx context.Context, token string) (*teams_repo.Participation, error) {
|
func (s *ParticipationService) GetParticipation(ctx context.Context, token string) (*Participation, error) {
|
||||||
return s.teamsRepo.GetParticipation(ctx, token)
|
p, err := s.teamsRepo.GetParticipation(ctx, token)
|
||||||
|
return mapParticipation(p), err
|
||||||
}
|
}
|
||||||
func (s *ParticipationService) AnswerParticipation(ctx context.Context, token, answer string) (*teams_repo.Participation, error) {
|
func (s *ParticipationService) AnswerParticipation(ctx context.Context, token, answer string) (*Participation, error) {
|
||||||
return s.teamsRepo.AnswerParticipation(ctx, token, answer)
|
p, err := s.teamsRepo.AnswerParticipation(ctx, token, answer)
|
||||||
|
return mapParticipation(p), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Стабильные токены для разных целей сохраняют работоспособность ссылок при
|
// Стабильные токены для разных целей сохраняют работоспособность ссылок при
|
||||||
@@ -45,7 +47,7 @@ func participationToken(secret string, id int) string {
|
|||||||
return hex.EncodeToString(mac.Sum(nil))
|
return hex.EncodeToString(mac.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
func participationEmail(p *teams_repo.Participation, baseURL, token string, location *time.Location) email_sender.Message {
|
func participationEmail(p *Participation, baseURL, token string, location *time.Location) email_sender.Message {
|
||||||
link := strings.TrimRight(baseURL, "/") + "/participation#token=" + token
|
link := strings.TrimRight(baseURL, "/") + "/participation#token=" + token
|
||||||
date := p.StartAt.In(location).Format("02.01.2006 в 15:04 (MST, UTC-07:00)")
|
date := p.StartAt.In(location).Format("02.01.2006 в 15:04 (MST, UTC-07:00)")
|
||||||
body := fmt.Sprintf("Команда «%s», подтвердите участие в игре «%s».\nДата: %s\nУчастников: %d\n\nПодтвердить участие: %s&answer=confirmed\nОтменить участие: %s&answer=cancelled\n\nОтвет можно дать до начала игры. Решение окончательное: изменить его нельзя. Без ответа запись сохраняется со статусом «Ожидает ответа».\nНе пересылайте письмо: ссылка позволяет принять решение за команду.", p.TeamName, p.GameName, date, p.ParticipantsCount, link, link)
|
body := fmt.Sprintf("Команда «%s», подтвердите участие в игре «%s».\nДата: %s\nУчастников: %d\n\nПодтвердить участие: %s&answer=confirmed\nОтменить участие: %s&answer=cancelled\n\nОтвет можно дать до начала игры. Решение окончательное: изменить его нельзя. Без ответа запись сохраняется со статусом «Ожидает ответа».\nНе пересылайте письмо: ссылка позволяет принять решение за команду.", p.TeamName, p.GameName, date, p.ParticipantsCount, link, link)
|
||||||
@@ -89,7 +91,7 @@ func (s *ParticipationService) RunParticipationEmails(ctx context.Context, sende
|
|||||||
func (s *ParticipationService) sendParticipationEmails(ctx context.Context, sender email_sender.IEmailSender, secret string, location *time.Location) error {
|
func (s *ParticipationService) sendParticipationEmails(ctx context.Context, sender email_sender.IEmailSender, secret string, location *time.Location) error {
|
||||||
for ctx.Err() == nil {
|
for ctx.Err() == nil {
|
||||||
jobCtx, cancel := context.WithTimeout(ctx, time.Minute)
|
jobCtx, cancel := context.WithTimeout(ctx, time.Minute)
|
||||||
p, err := s.teamsRepo.ClaimParticipationEmail(jobCtx)
|
repoParticipation, err := s.teamsRepo.ClaimParticipationEmail(jobCtx)
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if errors.Is(err, pgx.ErrNoRows) {
|
||||||
cancel()
|
cancel()
|
||||||
return nil
|
return nil
|
||||||
@@ -98,6 +100,7 @@ func (s *ParticipationService) sendParticipationEmails(ctx context.Context, send
|
|||||||
cancel()
|
cancel()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
p := mapParticipation(repoParticipation)
|
||||||
token := participationToken(secret, p.TeamID)
|
token := participationToken(secret, p.TeamID)
|
||||||
err = s.teamsRepo.SetParticipationToken(jobCtx, p.TeamID, token)
|
err = s.teamsRepo.SetParticipationToken(jobCtx, p.TeamID, token)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user