generated from VLADIMIR/template
up
This commit is contained in:
@@ -27,6 +27,7 @@ import (
|
||||
"evening_detective_server/internal/services/file_service"
|
||||
"evening_detective_server/internal/services/game_service"
|
||||
"evening_detective_server/internal/services/mcp_service"
|
||||
"evening_detective_server/internal/services/participation_service"
|
||||
"evening_detective_server/internal/services/scenarios_service"
|
||||
"evening_detective_server/internal/services/ui_service"
|
||||
"evening_detective_server/internal/services/users_service"
|
||||
@@ -148,7 +149,8 @@ func main() {
|
||||
os.Getenv("APP_BASE_URL"),
|
||||
)
|
||||
|
||||
go gameService.RunParticipationEmails(context.Background(), emailSender, os.Getenv("PARTICIPATION_SECRET"), os.Getenv("GAME_TIMEZONE"))
|
||||
participationService := participation_service.NewParticipationService(teamRepo, os.Getenv("APP_BASE_URL"))
|
||||
go participationService.RunParticipationEmails(context.Background(), emailSender, os.Getenv("PARTICIPATION_SECRET"), os.Getenv("GAME_TIMEZONE"))
|
||||
|
||||
// Create a listener on TCP port
|
||||
lis, err := net.Listen("tcp", ":8080")
|
||||
@@ -197,6 +199,7 @@ func main() {
|
||||
fileService,
|
||||
scenarioService,
|
||||
gameService,
|
||||
participationService,
|
||||
screenRepo,
|
||||
mapRepo,
|
||||
),
|
||||
|
||||
@@ -31,7 +31,7 @@ func participationError(err error) string {
|
||||
}
|
||||
func (s *server) GetParticipation(ctx context.Context, req *proto.GetParticipationReq) (*proto.ParticipationRsp, error) {
|
||||
_ = grpc.SetHeader(ctx, metadata.Pairs("Cache-Control", "no-store"))
|
||||
p, err := s.gameService.GetParticipation(ctx, req.Token)
|
||||
p, err := s.participationService.GetParticipation(ctx, req.Token)
|
||||
result := mapParticipationResponse(p)
|
||||
if err != nil {
|
||||
result.Error = participationError(err)
|
||||
@@ -43,7 +43,7 @@ func (s *server) AnswerParticipation(ctx context.Context, req *proto.AnswerParti
|
||||
if req.Answer != "confirmed" && req.Answer != "cancelled" {
|
||||
return &proto.ParticipationRsp{Error: "Выберите подтверждение или отмену участия"}, nil
|
||||
}
|
||||
p, err := s.gameService.AnswerParticipation(ctx, req.Token, req.Answer)
|
||||
p, err := s.participationService.AnswerParticipation(ctx, req.Token, req.Answer)
|
||||
result := mapParticipationResponse(p)
|
||||
if err != nil {
|
||||
result.Error = participationError(err)
|
||||
|
||||
+18
-14
@@ -6,6 +6,7 @@ import (
|
||||
"evening_detective_server/internal/repos/screen_sessions_repo"
|
||||
"evening_detective_server/internal/services/file_service"
|
||||
"evening_detective_server/internal/services/game_service"
|
||||
"evening_detective_server/internal/services/participation_service"
|
||||
"evening_detective_server/internal/services/scenarios_service"
|
||||
"evening_detective_server/internal/services/ui_service"
|
||||
"evening_detective_server/internal/services/users_service"
|
||||
@@ -18,13 +19,14 @@ var stringTools = string_tools.NewStringTools()
|
||||
type server struct {
|
||||
proto.UnsafeEveningDetectiveServerServer
|
||||
|
||||
usersService *users_service.UsersService
|
||||
uiService *ui_service.UiService
|
||||
fileService *file_service.FileService
|
||||
scenarioService *scenarios_service.ScenarioService
|
||||
gameService *game_service.GameService
|
||||
screenRepo *screen_sessions_repo.ScreenSessionsRepo
|
||||
mapRepo *game_map_repo.Repo
|
||||
usersService *users_service.UsersService
|
||||
uiService *ui_service.UiService
|
||||
fileService *file_service.FileService
|
||||
scenarioService *scenarios_service.ScenarioService
|
||||
gameService *game_service.GameService
|
||||
participationService *participation_service.ParticipationService
|
||||
screenRepo *screen_sessions_repo.ScreenSessionsRepo
|
||||
mapRepo *game_map_repo.Repo
|
||||
}
|
||||
|
||||
func NewServer(
|
||||
@@ -33,16 +35,18 @@ func NewServer(
|
||||
fileService *file_service.FileService,
|
||||
scenarioService *scenarios_service.ScenarioService,
|
||||
gameService *game_service.GameService,
|
||||
participationService *participation_service.ParticipationService,
|
||||
screenRepo *screen_sessions_repo.ScreenSessionsRepo,
|
||||
mapRepo *game_map_repo.Repo,
|
||||
) proto.EveningDetectiveServerServer {
|
||||
return &server{
|
||||
usersService: usersService,
|
||||
uiService: uiService,
|
||||
fileService: fileService,
|
||||
scenarioService: scenarioService,
|
||||
gameService: gameService,
|
||||
screenRepo: screenRepo,
|
||||
mapRepo: mapRepo,
|
||||
usersService: usersService,
|
||||
uiService: uiService,
|
||||
fileService: fileService,
|
||||
scenarioService: scenarioService,
|
||||
gameService: gameService,
|
||||
participationService: participationService,
|
||||
screenRepo: screenRepo,
|
||||
mapRepo: mapRepo,
|
||||
}
|
||||
}
|
||||
|
||||
+20
-17
@@ -1,4 +1,4 @@
|
||||
package game_service
|
||||
package participation_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"evening_detective_server/internal/modules/email_sender"
|
||||
"evening_detective_server/internal/repos/teams_repo"
|
||||
"evening_detective_server/internal/services/game_service"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
@@ -88,7 +89,7 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
exec(strings.Split(string(data), "-- +goose Down")[0])
|
||||
}
|
||||
repo := teams_repo.NewTeamsRepo(pool)
|
||||
service := &GameService{teamsRepo: repo, appBaseURL: "https://example.org"}
|
||||
service := NewParticipationService(repo, "https://example.org")
|
||||
state := func(id int, want string) {
|
||||
t.Helper()
|
||||
team, err := repo.GetTeamByID(ctx, id)
|
||||
@@ -98,9 +99,10 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
}
|
||||
state(1, "pending")
|
||||
state(2, "pending") // Existing teams are not changed by the migration.
|
||||
exec(`UPDATE teams SET captain_email='captain@example.org',participants_count=5;
|
||||
// Model registration output explicitly: direct SQL inserts do not auto-confirm teams.
|
||||
exec(`UPDATE teams SET captain_email='captain@example.org',participants_count=5 WHERE id=1;
|
||||
INSERT INTO teams(game_id,created_at,captain_email) SELECT 1,start_at-INTERVAL '24 hours','boundary@example.org' FROM games WHERE id=1;
|
||||
INSERT INTO teams(game_id,created_at,captain_email) SELECT 1,start_at-INTERVAL '24 hours'+INTERVAL '1 microsecond','late@example.org' FROM games WHERE id=1;
|
||||
INSERT INTO teams(game_id,created_at,captain_email,participation_status,participation_source,participation_answered_at) SELECT 1,start_at-INTERVAL '24 hours'+INTERVAL '1 microsecond','late@example.org','confirmed','automatic',NOW() FROM games WHERE id=1;
|
||||
INSERT INTO teams(game_id,captain_email) VALUES(2,'future@example.org');
|
||||
INSERT INTO teams(game_id,created_at) SELECT 1,start_at-INTERVAL '25 hours' FROM games WHERE id=1;`)
|
||||
state(3, "pending")
|
||||
@@ -156,7 +158,7 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
}
|
||||
sender.err = nil
|
||||
// Expired lease is recovered after a worker restart.
|
||||
exec(`UPDATE teams SET participation_next_attempt_at=NOW()-INTERVAL '1 minute' WHERE id=3`)
|
||||
exec(`UPDATE teams SET participation_next_attempt_at=NOW()-INTERVAL '1 minute' WHERE id IN (1,3)`)
|
||||
if err := service.sendParticipationEmails(ctx, sender, "secret", time.UTC); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -171,19 +173,19 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
t.Fatal("email retry exceeded the eight-attempt limit")
|
||||
}
|
||||
token := participationToken("secret", 1)
|
||||
if _, err := repo.GetParticipation(ctx, "bad"); !errors.Is(err, teams_repo.ErrParticipationLink) {
|
||||
if _, err := service.GetParticipation(ctx, "bad"); !errors.Is(err, teams_repo.ErrParticipationLink) {
|
||||
t.Fatalf("invalid token: %v", err)
|
||||
}
|
||||
if _, err := repo.GetParticipation(ctx, strings.Repeat("0", 64)); !errors.Is(err, teams_repo.ErrParticipationLink) {
|
||||
if _, err := service.GetParticipation(ctx, strings.Repeat("0", 64)); !errors.Is(err, teams_repo.ErrParticipationLink) {
|
||||
t.Fatalf("unknown token: %v", err)
|
||||
}
|
||||
if _, err := repo.GetParticipation(ctx, token); err != nil {
|
||||
if _, err := service.GetParticipation(ctx, token); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state(1, "pending") // Viewing a link never answers.
|
||||
answers := make(chan error, 2)
|
||||
for _, answer := range []string{"confirmed", "cancelled"} {
|
||||
go func() { _, err := repo.AnswerParticipation(ctx, token, answer); answers <- err }()
|
||||
go func() { _, err := service.AnswerParticipation(ctx, token, answer); answers <- err }()
|
||||
}
|
||||
success, conflict := 0, 0
|
||||
for range 2 {
|
||||
@@ -199,16 +201,16 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
if success != 1 || conflict != 1 {
|
||||
t.Fatalf("concurrent answers: success=%d conflict=%d", success, conflict)
|
||||
}
|
||||
p, err := repo.GetParticipation(ctx, token)
|
||||
p, err := service.GetParticipation(ctx, token)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := repo.AnswerParticipation(ctx, token, p.Status); err != nil {
|
||||
if _, err := service.AnswerParticipation(ctx, token, p.Status); err != nil {
|
||||
t.Fatalf("same answer not idempotent: %v", err)
|
||||
}
|
||||
// Cancellation excludes teams from shared active listings and password-based gameplay.
|
||||
token3 := participationToken("secret", 3)
|
||||
if _, err := repo.AnswerParticipation(ctx, token3, "cancelled"); err != nil {
|
||||
if _, err := service.AnswerParticipation(ctx, token3, "cancelled"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
active, err := repo.GetActiveTeamsByGameID(ctx, 1)
|
||||
@@ -220,13 +222,14 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
t.Fatal("cancelled team in active list")
|
||||
}
|
||||
}
|
||||
if _, _, _, err := service.GetTeamActions(ctx, 3, "secret"); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
gameService := game_service.NewGameService(nil, repo, nil, nil, "", nil, nil, nil, nil, nil, nil, nil, "")
|
||||
if _, _, _, err := gameService.GetTeamActions(ctx, 3, "secret"); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
t.Fatalf("cancelled team got story: %v", err)
|
||||
}
|
||||
if err := service.AddTeamAction(ctx, 3, "secret", "anything", false); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
if err := gameService.AddTeamAction(ctx, 3, "secret", "anything", false); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
t.Fatalf("cancelled team added action: %v", err)
|
||||
}
|
||||
if _, err := service.GetTeamAnswers(ctx, 3, "secret"); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
if _, err := gameService.GetTeamAnswers(ctx, 3, "secret"); !errors.Is(err, teams_repo.ErrTeamNotFound) {
|
||||
t.Fatalf("cancelled team got answers: %v", err)
|
||||
}
|
||||
token6 := participationToken("secret", 6)
|
||||
@@ -234,11 +237,11 @@ func TestParticipationPostgres(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
exec(`UPDATE games SET status='run' WHERE id=1`)
|
||||
if _, err := repo.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, teams_repo.ErrParticipationClosed) {
|
||||
if _, err := service.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, teams_repo.ErrParticipationClosed) {
|
||||
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`)
|
||||
if _, err := repo.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, teams_repo.ErrParticipationClosed) {
|
||||
if _, err := service.AnswerParticipation(ctx, token6, "confirmed"); !errors.Is(err, teams_repo.ErrParticipationClosed) {
|
||||
t.Fatalf("past scheduled start accepted answer: %v", err)
|
||||
}
|
||||
state(6, "pending")
|
||||
+14
-5
@@ -1,4 +1,4 @@
|
||||
package game_service
|
||||
package participation_service
|
||||
|
||||
import (
|
||||
"context"
|
||||
@@ -19,10 +19,19 @@ import (
|
||||
"github.com/jackc/pgx/v5"
|
||||
)
|
||||
|
||||
func (s *GameService) GetParticipation(ctx context.Context, token string) (*teams_repo.Participation, error) {
|
||||
type ParticipationService struct {
|
||||
teamsRepo *teams_repo.TeamsRepo
|
||||
appBaseURL string
|
||||
}
|
||||
|
||||
func NewParticipationService(teamsRepo *teams_repo.TeamsRepo, appBaseURL string) *ParticipationService {
|
||||
return &ParticipationService{teamsRepo: teamsRepo, appBaseURL: appBaseURL}
|
||||
}
|
||||
|
||||
func (s *ParticipationService) GetParticipation(ctx context.Context, token string) (*teams_repo.Participation, error) {
|
||||
return s.teamsRepo.GetParticipation(ctx, token)
|
||||
}
|
||||
func (s *GameService) AnswerParticipation(ctx context.Context, token, answer string) (*teams_repo.Participation, error) {
|
||||
func (s *ParticipationService) AnswerParticipation(ctx context.Context, token, answer string) (*teams_repo.Participation, error) {
|
||||
return s.teamsRepo.AnswerParticipation(ctx, token, answer)
|
||||
}
|
||||
|
||||
@@ -46,7 +55,7 @@ func participationEmail(p *teams_repo.Participation, baseURL, token string, loca
|
||||
return email_sender.Message{To: p.Email, Subject: "Подтвердите участие в «Вечернем детективе»", Body: body, HTML: markup}
|
||||
}
|
||||
|
||||
func (s *GameService) RunParticipationEmails(ctx context.Context, sender email_sender.IEmailSender, secret, timezone string) {
|
||||
func (s *ParticipationService) RunParticipationEmails(ctx context.Context, sender email_sender.IEmailSender, secret, timezone string) {
|
||||
base, err := url.Parse(s.appBaseURL)
|
||||
if err != nil || base.Host == "" || (base.Scheme != "https" && base.Scheme != "http") || base.User != nil || base.RawQuery != "" || base.Fragment != "" || secret == "" {
|
||||
log.Print("participation email worker disabled: configure APP_BASE_URL and PARTICIPATION_SECRET")
|
||||
@@ -74,7 +83,7 @@ func (s *GameService) RunParticipationEmails(ctx context.Context, sender email_s
|
||||
}
|
||||
}
|
||||
|
||||
func (s *GameService) 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 {
|
||||
jobCtx, cancel := context.WithTimeout(ctx, time.Minute)
|
||||
p, err := s.teamsRepo.ClaimParticipationEmail(jobCtx)
|
||||
Reference in New Issue
Block a user