add registration

This commit is contained in:
2026-09-13 01:42:22 +07:00
parent 0723644d3f
commit c12f81a7c4
20 changed files with 517 additions and 33 deletions
+3
View File
@@ -25,3 +25,6 @@ S3_PASSWORD=your_password
S3_BUCKET=evening-detective
FILE_PREFIX_DOMAIN=http://0.0.0.0:8090/api/files/
# Секрет webhook Яндекс Форм. Пустое значение отключает регистрацию.
YANDEX_FORMS_TOKEN=
+59
View File
@@ -128,3 +128,62 @@ MCP-эндпоинт: `http://localhost:8090/api/mcp`. ВНИМАНИЕ: REST-ga
}
}
```
## Регистрация команды из Яндекс Форм
`POST /api/games/{id}/registrations` — отдельный HTTP webhook, без пользовательского JWT.
Капитан не должен иметь учётную запись. Создаётся обычная команда с автоматически
сгенерированным паролем; почта капитана и количество участников видны организатору
в списке команд игры. Письма автоматически не отправляются.
Перед запуском примените миграции и задайте `YANDEX_FORMS_TOKEN` в окружении сервера
(случайный секрет, например результат `openssl rand -hex 32`). Пустое значение отключает ручку.
Секрет настраивается только в интеграции формы, не в её публичных полях.
В Яндекс Формах откройте **Интеграции → Запрос заданным методом**:
- Метод: `POST`.
- URL: `https://<адрес-сервера>/api/games/42/registrations`, где `42` — ID игры.
- Заголовок `Authorization`: `Bearer <YANDEX_FORMS_TOKEN>`.
- Заголовок `Content-Type`: `application/json`.
- Тело запроса:
```json
{
"name": "Следствие ведут котики",
"email": "captain@example.org",
"participantsCount": 5
}
```
Подставьте вместо примеров переменные ответов на вопросы «Название команды»,
«Почта капитана», «Количество участников». Для строк используйте фильтр JSON,
чтобы кавычки и переносы строк корректно экранировались. Число участников передаётся
целым JSON-числом без кавычек: от 1 до 2147483647. Название — 1–255 символов после
удаления пробелов по краям; email — одиночный адрес длиной до 254 байт.
Яндекс передаёт заголовок `X-Delivery-Id`: он обязателен и используется для защиты
от повторной доставки. Один ID доставки в пределах игры возвращает ту же команду,
не меняя её данные и пароль. Новая отправка формы с другим ID создаёт новую команду,
даже если почта и название совпадают. Для ручного теста задайте этот заголовок сами.
Успех: `200 {"id":123}`. Пароль команды в ответ webhook не передаётся.
Регистрация новой команды доступна только для игры в статусе `draft`.
Повтор уже принятой доставки успешен и после старта игры.
Ошибки: `400` — поля/JSON/ID доставки; `401` — токен; `409` — игра отсутствует или
регистрация закрыта; `503` — интеграция выключена; `500` — внутренняя ошибка.
Лимит тела — 16 КиБ, время обработки — до 4 секунд.
Сервис должен быть доступен по HTTPS и IPv6: Яндекс Формы отправляют интеграционные
запросы только с IPv6. При фильтрации входящего трафика разрешите сеть
`2a02:6b8:c00::/40`. Подробности и настройка переменных — в
[официальной инструкции Яндекс Форм](https://yandex.ru/support/forms/ru/send-request).
Проверка SQL и повторов на отдельной тестовой PostgreSQL:
```sh
TEST_DATABASE_URL='postgres://...' go test ./internal/repos/teams_repo -run TestRegistrationPostgres -count=1
```
Тест создаёт и удаляет собственную схему; требуется право `CREATE SCHEMA`.
+8 -6
View File
@@ -1453,12 +1453,14 @@ message GetScreenResultsRsp {
}
message Team {
int32 id = 1;
string name = 2;
string status = 3;
string password = 4;
int32 actions_count = 5;
repeated Application applications = 6;
int32 id = 1;
string name = 2;
string status = 3;
string password = 4;
int32 actions_count = 5;
repeated Application applications = 6;
string captain_email = 7;
int32 participants_count = 8;
}
message GetGameReq {
Binary file not shown.
+1
View File
@@ -268,6 +268,7 @@ func main() {
}
mainMux := http.NewServeMux()
mainMux.Handle("POST /api/games/{id}/registrations", app.TeamRegistrationHandler(os.Getenv("YANDEX_FORMS_TOKEN"), gameService.RegisterTeam))
docsHandler := v5emb.New("API", "/swagger/doc.json", "/docs/")
mainMux.Handle("/docs/", docsHandler)
@@ -4019,6 +4019,13 @@
"type": "object",
"$ref": "#/definitions/evening_detective_serverApplication"
}
},
"captainEmail": {
"type": "string"
},
"participantsCount": {
"type": "integer",
"format": "int32"
}
}
},
+1
View File
@@ -12,6 +12,7 @@ services:
ports:
- "8080:8080"
environment:
YANDEX_FORMS_TOKEN: ${YANDEX_FORMS_TOKEN:-}
DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/evening_detective_server?sslmode=disable"
SMTP_HOST: mail.crabs-games.art
SMTP_PORT: 465
+5 -1
View File
@@ -19,6 +19,10 @@ func mapGame(
endedAt = timestamppb.New(*o.EndedAt)
}
teams := make([]*proto.Team, 0, len(o.Teams))
for _, team := range o.Teams {
teams = append(teams, mapTeamDetails(team))
}
return &proto.Game{
Id: int32(o.ID),
Name: o.Name,
@@ -27,7 +31,7 @@ func mapGame(
Status: o.Status,
Format: o.Format,
Scenario: mapScenario(o.Scenario),
Teams: mapTeams(o.Teams),
Teams: teams,
StartedAt: startedAt,
EndedAt: endedAt,
}
+7
View File
@@ -18,6 +18,13 @@ func mapTeam(
}
}
func mapTeamDetails(o *game_service.Team) *proto.Team {
team := mapTeam(o)
team.CaptainEmail = o.CaptainEmail
team.ParticipantsCount = o.ParticipantsCount
return team
}
func mapTeams(
o []*game_service.Team,
) []*proto.Team {
+65
View File
@@ -0,0 +1,65 @@
package app
import (
"context"
"crypto/subtle"
"encoding/json"
"errors"
"io"
"net/http"
"strconv"
"time"
"evening_detective_server/internal/repos/teams_repo"
"evening_detective_server/internal/services/game_service"
)
// TeamRegistrationHandler принимает webhook Яндекс Форм отдельно от пользовательского JWT.
func TeamRegistrationHandler(token string, register func(context.Context, int, string, game_service.Registration) (int, error)) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
fail := func(code int, message string) {
w.WriteHeader(code)
_ = json.NewEncoder(w).Encode(map[string]string{"error": message})
}
if token == "" {
fail(http.StatusServiceUnavailable, "Интеграция регистрации не настроена")
return
}
if subtle.ConstantTimeCompare([]byte(r.Header.Get("Authorization")), []byte("Bearer "+token)) != 1 {
fail(http.StatusUnauthorized, "Неверный токен интеграции")
return
}
gameID, err := strconv.ParseInt(r.PathValue("id"), 10, 32)
deliveryID := r.Header.Get("X-Delivery-Id")
if err != nil || gameID <= 0 || deliveryID == "" || len(deliveryID) > 255 {
fail(http.StatusBadRequest, "Требуются ID игры и заголовок X-Delivery-Id (до 255 байт)")
return
}
decoder := json.NewDecoder(http.MaxBytesReader(w, r.Body, 16<<10))
decoder.DisallowUnknownFields()
var registration game_service.Registration
if err := decoder.Decode(&registration); err != nil {
fail(http.StatusBadRequest, "Некорректный JSON регистрации")
return
}
if err := decoder.Decode(new(any)); err != io.EOF {
fail(http.StatusBadRequest, "Ожидается один JSON-объект")
return
}
ctx, cancel := context.WithTimeout(r.Context(), 4*time.Second)
defer cancel()
id, err := register(ctx, int(gameID), deliveryID, registration)
switch {
case errors.Is(err, game_service.ErrInvalidRegistration):
fail(http.StatusBadRequest, err.Error())
case errors.Is(err, teams_repo.ErrRegistrationClosed):
fail(http.StatusConflict, err.Error())
case err != nil:
fail(http.StatusInternalServerError, "Не удалось зарегистрировать команду")
default:
_ = json.NewEncoder(w).Encode(map[string]int{"id": id})
}
})
}
+77
View File
@@ -0,0 +1,77 @@
package app
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"evening_detective_server/internal/repos/teams_repo"
"evening_detective_server/internal/services/game_service"
)
func TestTeamRegistrationHandler(t *testing.T) {
valid := `{"name":"Команда","email":"captain@example.org","participantsCount":5}`
for _, tc := range []struct {
name, token, auth, gameID, deliveryID, body string
serviceErr error
want int
called bool
}{
{"success", "secret", "Bearer secret", "42", "delivery", valid, nil, 200, true},
{"disabled", "", "Bearer secret", "42", "delivery", valid, nil, 503, false},
{"no token", "secret", "", "42", "delivery", valid, nil, 401, false},
{"wrong token", "secret", "Bearer wrong", "42", "delivery", valid, nil, 401, false},
{"bad game", "secret", "Bearer secret", "0", "delivery", valid, nil, 400, false},
{"no delivery", "secret", "Bearer secret", "42", "", valid, nil, 400, false},
{"malformed", "secret", "Bearer secret", "42", "delivery", `{`, nil, 400, false},
{"extra object", "secret", "Bearer secret", "42", "delivery", valid + ` {}`, nil, 400, false},
{"unknown field", "secret", "Bearer secret", "42", "delivery", `{"extra":1}`, nil, 400, false},
{"too large", "secret", "Bearer secret", "42", "delivery", `{"name":"` + strings.Repeat("a", 17000) + `"}`, nil, 400, false},
{"invalid fields", "secret", "Bearer secret", "42", "delivery", valid, game_service.ErrInvalidRegistration, 400, true},
{"closed", "secret", "Bearer secret", "42", "delivery", valid, teams_repo.ErrRegistrationClosed, 409, true},
{"internal", "secret", "Bearer secret", "42", "delivery", valid, errors.New("private database detail"), 500, true},
} {
t.Run(tc.name, func(t *testing.T) {
called := false
mux := http.NewServeMux()
mux.Handle("POST /api/games/{id}/registrations", TeamRegistrationHandler(tc.token, func(ctx context.Context, gameID int, deliveryID string, registration game_service.Registration) (int, error) {
called = true
if gameID != 42 || deliveryID != "delivery" || registration.Name != "Команда" || registration.Email != "captain@example.org" || registration.ParticipantsCount != 5 {
t.Fatal("incorrect registration passed to service")
}
if _, ok := ctx.Deadline(); !ok {
t.Fatal("missing deadline")
}
return 7, tc.serviceErr
}))
req := httptest.NewRequest(http.MethodPost, "/api/games/"+tc.gameID+"/registrations", strings.NewReader(tc.body))
req.Header.Set("Authorization", tc.auth)
req.Header.Set("X-Delivery-Id", tc.deliveryID)
res := httptest.NewRecorder()
mux.ServeHTTP(res, req)
if res.Code != tc.want || called != tc.called {
t.Fatalf("status=%d called=%v body=%s", res.Code, called, res.Body.String())
}
if tc.want == 200 && res.Body.String() != "{\"id\":7}\n" {
t.Fatal(res.Body.String())
}
if strings.Contains(res.Body.String(), "private database detail") {
t.Fatal("private error exposed")
}
})
}
}
func TestCaptainContactOnlyInGameDetails(t *testing.T) {
team := &game_service.Team{ID: 1, CaptainEmail: "captain@example.org", ParticipantsCount: 5}
details := mapGame(&game_service.Game{Teams: []*game_service.Team{team}})
if details.Teams[0].CaptainEmail != team.CaptainEmail || details.Teams[0].ParticipantsCount != 5 {
t.Fatal("registration details missing from organizer view")
}
if mapTeam(team).CaptainEmail != "" {
t.Fatal("captain email exposed in shared team/results mapping")
}
}
+7 -5
View File
@@ -1,9 +1,11 @@
package repos
type Team struct {
ID int
Name string
Status string
Password string
GameId int
CaptainEmail string
ParticipantsCount int32
ID int
Name string
Status string
Password string
GameId int
}
@@ -0,0 +1,114 @@
package teams_repo
import (
"context"
"errors"
"fmt"
"os"
"strings"
"testing"
"time"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
func TestRegistrationPostgres(t *testing.T) {
dsn := os.Getenv("TEST_DATABASE_URL")
if dsn == "" {
t.Skip("set TEST_DATABASE_URL to run PostgreSQL integration check")
}
ctx := context.Background()
admin, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatal(err)
}
defer admin.Close()
schema := fmt.Sprintf("registration_test_%d", time.Now().UnixNano())
quoted := pgx.Identifier{schema}.Sanitize()
if _, err := admin.Exec(ctx, "CREATE SCHEMA "+quoted); err != nil {
t.Fatal(err)
}
defer func() {
if _, err := admin.Exec(ctx, "DROP SCHEMA "+quoted+" CASCADE"); err != nil {
t.Error(err)
}
}()
config, err := pgxpool.ParseConfig(dsn)
if err != nil {
t.Fatal(err)
}
config.ConnConfig.RuntimeParams["search_path"] = schema
pool, err := pgxpool.NewWithConfig(ctx, config)
if err != nil {
t.Fatal(err)
}
defer pool.Close()
_, err = pool.Exec(ctx, `CREATE TABLE games (id SERIAL PRIMARY KEY, status TEXT DEFAULT 'draft');
CREATE TABLE teams (id SERIAL PRIMARY KEY, name VARCHAR(255), creator_id INTEGER,
game_id INTEGER REFERENCES games(id), password TEXT, status TEXT DEFAULT 'register', created_at TIMESTAMP DEFAULT NOW());
INSERT INTO games DEFAULT VALUES; INSERT INTO games DEFAULT VALUES;`)
if err != nil {
t.Fatal(err)
}
migration, err := os.ReadFile("../../../migrations/20260913130000_add_team_registration.sql")
if err != nil {
t.Fatal(err)
}
if _, err := pool.Exec(ctx, strings.Split(string(migration), "-- +goose Down")[0]); err != nil {
t.Fatal(err)
}
repo := NewTeamsRepo(pool)
type result struct {
id int
err error
}
results := make(chan result, 8)
for range 8 {
go func() {
id, err := repo.RegisterTeam(ctx, 1, "delivery", "Team", "captain@example.org", 5, "abcdefgh")
results <- result{id, err}
}()
}
var id int
for range 8 {
got := <-results
if got.err != nil {
t.Fatal(got.err)
}
if id != 0 && got.id != id {
t.Fatal("concurrent delivery created a duplicate")
}
id = got.id
}
team, err := repo.GetTeamByID(ctx, id)
if err != nil {
t.Fatal(err)
}
if team.Name != "Team" || team.CaptainEmail != "captain@example.org" || team.ParticipantsCount != 5 || team.Password != "abcdefgh" {
t.Fatalf("incorrect team: %+v", team)
}
var noUser bool
if err := pool.QueryRow(ctx, "SELECT creator_id IS NULL FROM teams WHERE id=$1", id).Scan(&noUser); err != nil || !noUser {
t.Fatalf("captain has a user: %v", err)
}
teams, err := repo.GetTeamsByGameID(ctx, 1)
if err != nil || len(teams) != 1 || teams[0].CaptainEmail != team.CaptainEmail {
t.Fatalf("teams=%v err=%v", teams, err)
}
if _, err := pool.Exec(ctx, "UPDATE games SET status='run' WHERE id=1"); err != nil {
t.Fatal(err)
}
if got, err := repo.RegisterTeam(ctx, 1, "delivery", "Changed", "other@example.org", 9, "different"); err != nil || got != id {
t.Fatalf("retry after start: %d %v", got, err)
}
if _, err := repo.RegisterTeam(ctx, 1, "new", "Team", "captain@example.org", 5, "abcdefgh"); !errors.Is(err, ErrRegistrationClosed) {
t.Fatalf("registration after start: %v", err)
}
if _, err := repo.RegisterTeam(ctx, 999, "new", "Team", "captain@example.org", 5, "abcdefgh"); !errors.Is(err, ErrRegistrationClosed) {
t.Fatalf("missing game: %v", err)
}
if otherID, err := repo.RegisterTeam(ctx, 2, "delivery", "Team", "captain@example.org", 5, "abcdefgh"); err != nil || otherID == id {
t.Fatalf("other game: %d %v", otherID, err)
}
}
+33
View File
@@ -30,6 +30,8 @@ func (r *TeamsRepo) GetTeamByID(ctx context.Context, id int) (*repos.Team, error
`SELECT
id,
name,
captain_email,
participants_count,
status,
password,
game_id
@@ -40,6 +42,8 @@ func (r *TeamsRepo) GetTeamByID(ctx context.Context, id int) (*repos.Team, error
err := row.Scan(
&team.ID,
&team.Name,
&team.CaptainEmail,
&team.ParticipantsCount,
&team.Status,
&team.Password,
&team.GameId,
@@ -63,6 +67,8 @@ func (r *TeamsRepo) GetTeamsByGameID(
`SELECT
id,
name,
captain_email,
participants_count,
status,
password
FROM teams
@@ -81,6 +87,8 @@ func (r *TeamsRepo) GetTeamsByGameID(
err := rows.Scan(
&team.ID,
&team.Name,
&team.CaptainEmail,
&team.ParticipantsCount,
&team.Status,
&team.Password,
)
@@ -155,3 +163,28 @@ func (r *TeamsRepo) DeleteTeam(ctx context.Context, teamId int) error {
}
return nil
}
var ErrRegistrationClosed = errors.New("Игра не найдена или регистрация уже закрыта")
// RegisterTeam сохраняет одну команду на доставку, включая одновременные повторы.
func (r *TeamsRepo) RegisterTeam(ctx context.Context, gameID int, deliveryID, name, email string, participantsCount int32, password string) (int, error) {
var id int
// Повтор уже принятой доставки успешен и после старта игры.
err := r.pool.QueryRow(ctx, `SELECT id FROM teams WHERE game_id = $1 AND registration_delivery_id = $2`, gameID, deliveryID).Scan(&id)
if err == nil {
return id, nil
}
if !errors.Is(err, pgx.ErrNoRows) {
return 0, err
}
err = r.pool.QueryRow(ctx, `
INSERT INTO teams (game_id, registration_delivery_id, name, captain_email, participants_count, password)
SELECT id, $2, $3, $4, $5, $6 FROM games WHERE id = $1 AND status = 'draft'
ON CONFLICT (game_id, registration_delivery_id)
DO UPDATE SET registration_delivery_id = EXCLUDED.registration_delivery_id
RETURNING id`, gameID, deliveryID, name, email, participantsCount, password).Scan(&id)
if errors.Is(err, pgx.ErrNoRows) {
return 0, ErrRegistrationClosed
}
return id, err
}
@@ -0,0 +1,42 @@
package game_service
import (
"context"
"errors"
"net/mail"
"strings"
"unicode/utf8"
)
var ErrInvalidRegistration = errors.New("Укажите название команды (до 255 символов), почту капитана и положительное количество участников")
type Registration struct {
Name string `json:"name"`
Email string `json:"email"`
ParticipantsCount int32 `json:"participantsCount"`
}
func (r *Registration) Validate() error {
r.Name = strings.TrimSpace(r.Name)
r.Email = strings.TrimSpace(r.Email)
address, err := mail.ParseAddress(r.Email)
if r.Name == "" || !utf8.ValidString(r.Name) || strings.ContainsRune(r.Name, 0) || utf8.RuneCountInString(r.Name) > 255 ||
err != nil || address.Address != r.Email || len(r.Email) > 254 || r.ParticipantsCount <= 0 {
return ErrInvalidRegistration
}
return nil
}
func (s *GameService) RegisterTeam(ctx context.Context, gameID int, deliveryID string, registration Registration) (int, error) {
if err := registration.Validate(); err != nil {
return 0, err
}
if gameID <= 0 || deliveryID == "" || len(deliveryID) > 255 || strings.ContainsRune(deliveryID, 0) {
return 0, ErrInvalidRegistration
}
password, err := s.passwordGenerator.Generate()
if err != nil {
return 0, err
}
return s.teamsRepo.RegisterTeam(ctx, gameID, deliveryID, registration.Name, registration.Email, registration.ParticipantsCount, password)
}
@@ -0,0 +1,32 @@
package game_service
import (
"strings"
"testing"
)
func TestRegistrationValidation(t *testing.T) {
for _, tc := range []struct {
name, email string
count int32
valid bool
}{
{" Команда ", " captain@example.org ", 5, true},
{strings.Repeat("я", 255), "captain@example.org", 1, true},
{"", "captain@example.org", 5, false},
{strings.Repeat("я", 256), "captain@example.org", 5, false},
{"team\x00", "captain@example.org", 5, false},
{"team", "Captain <captain@example.org>", 5, false},
{"team", "not-an-email", 5, false},
{"team", "captain@example.org", 0, false},
{"team", "captain@example.org", -1, false},
} {
r := Registration{Name: tc.name, Email: tc.email, ParticipantsCount: tc.count}
if err := r.Validate(); (err == nil) != tc.valid {
t.Fatalf("validation %q %q %d: %v", tc.name, tc.email, tc.count, err)
}
if tc.valid && (r.Name != strings.TrimSpace(tc.name) || r.Email != strings.TrimSpace(tc.email)) {
t.Fatal("spaces not trimmed")
}
}
}
+8 -6
View File
@@ -3,10 +3,12 @@ package game_service
import "evening_detective_server/internal/modules/storytelling"
type Team struct {
ID int
Name string
Status string
Password string
ActionsCount int
Applications []*storytelling.Application
CaptainEmail string
ParticipantsCount int32
ID int
Name string
Status string
Password string
ActionsCount int
Applications []*storytelling.Application
}
@@ -6,10 +6,12 @@ func mapTeam(
o *repos.Team,
) *Team {
return &Team{
ID: o.ID,
Name: o.Name,
Status: o.Status,
Password: o.Password,
ID: o.ID,
CaptainEmail: o.CaptainEmail,
ParticipantsCount: o.ParticipantsCount,
Name: o.Name,
Status: o.Status,
Password: o.Password,
}
}
@@ -0,0 +1,13 @@
-- +goose Up
ALTER TABLE teams
ADD COLUMN captain_email TEXT NOT NULL DEFAULT '',
ADD COLUMN participants_count INTEGER NOT NULL DEFAULT 0 CHECK (participants_count >= 0),
ADD COLUMN registration_delivery_id TEXT,
ADD CONSTRAINT teams_registration_delivery_unique UNIQUE (game_id, registration_delivery_id);
-- +goose Down
ALTER TABLE teams
DROP CONSTRAINT teams_registration_delivery_unique,
DROP COLUMN registration_delivery_id,
DROP COLUMN participants_count,
DROP COLUMN captain_email;
+29 -11
View File
@@ -6106,15 +6106,17 @@ func (x *GetScreenResultsRsp) GetTeams() []*TeamResult {
}
type Team struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
Password string `protobuf:"bytes,4,opt,name=password,proto3" json:"password,omitempty"`
ActionsCount int32 `protobuf:"varint,5,opt,name=actions_count,json=actionsCount,proto3" json:"actions_count,omitempty"`
Applications []*Application `protobuf:"bytes,6,rep,name=applications,proto3" json:"applications,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
state protoimpl.MessageState `protogen:"open.v1"`
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
Password string `protobuf:"bytes,4,opt,name=password,proto3" json:"password,omitempty"`
ActionsCount int32 `protobuf:"varint,5,opt,name=actions_count,json=actionsCount,proto3" json:"actions_count,omitempty"`
Applications []*Application `protobuf:"bytes,6,rep,name=applications,proto3" json:"applications,omitempty"`
CaptainEmail string `protobuf:"bytes,7,opt,name=captain_email,json=captainEmail,proto3" json:"captain_email,omitempty"`
ParticipantsCount int32 `protobuf:"varint,8,opt,name=participants_count,json=participantsCount,proto3" json:"participants_count,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Team) Reset() {
@@ -6189,6 +6191,20 @@ func (x *Team) GetApplications() []*Application {
return nil
}
func (x *Team) GetCaptainEmail() string {
if x != nil {
return x.CaptainEmail
}
return ""
}
func (x *Team) GetParticipantsCount() int32 {
if x != nil {
return x.ParticipantsCount
}
return 0
}
type GetGameReq struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"`
@@ -8832,14 +8848,16 @@ const file_main_proto_rawDesc = "" +
"\x13GetScreenResultsRsp\x12\x14\n" +
"\x05error\x18\x01 \x01(\tR\x05error\x12M\n" +
"\tquestions\x18\x02 \x03(\v2/.crabs.evening_detective_server.ResultsQuestionR\tquestions\x12@\n" +
"\x05teams\x18\x03 \x03(\v2*.crabs.evening_detective_server.TeamResultR\x05teams\"\xd4\x01\n" +
"\x05teams\x18\x03 \x03(\v2*.crabs.evening_detective_server.TeamResultR\x05teams\"\xa8\x02\n" +
"\x04Team\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x05R\x02id\x12\x12\n" +
"\x04name\x18\x02 \x01(\tR\x04name\x12\x16\n" +
"\x06status\x18\x03 \x01(\tR\x06status\x12\x1a\n" +
"\bpassword\x18\x04 \x01(\tR\bpassword\x12#\n" +
"\ractions_count\x18\x05 \x01(\x05R\factionsCount\x12O\n" +
"\fapplications\x18\x06 \x03(\v2+.crabs.evening_detective_server.ApplicationR\fapplications\"\x1c\n" +
"\fapplications\x18\x06 \x03(\v2+.crabs.evening_detective_server.ApplicationR\fapplications\x12#\n" +
"\rcaptain_email\x18\a \x01(\tR\fcaptainEmail\x12-\n" +
"\x12participants_count\x18\b \x01(\x05R\x11participantsCount\"\x1c\n" +
"\n" +
"GetGameReq\x12\x0e\n" +
"\x02id\x18\x01 \x01(\x05R\x02id\"\\\n" +