syntax = "proto3"; // Contract rules: // - fields inside each message are declared in ascending number order; // - published field numbers are never renumbered or reused; // - use ID, ID and URL for abbreviation fields so generated clients // keep the same naming convention across Go and TypeScript. package crabs.evening_detective_server; import "google/api/annotations.proto"; import "google/protobuf/timestamp.proto"; import "protoc-gen-openapiv2/options/annotations.proto"; import "google/api/httpbody.proto"; option go_package = "pkg/proto"; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { title : "Документация сервиса \"Вечерний детектив\""; version: "v0.1.0"; }; security_definitions: { security: { key : "BearerAuth"; value: { type : TYPE_API_KEY; in : IN_HEADER; name : "Authorization"; description: "Authentication token, prefixed by Bearer: Bearer "; } } } security: { security_requirement: { key : "BearerAuth"; value: {}; } } }; service EveningDetectiveServer { rpc DeviceHeartbeat(DeviceHeartbeatReq) returns (DeviceHeartbeatRsp) { option (google.api.http) = { post: "/api/devices/heartbeat" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Устройства"; summary : "Обновить присутствие подключения браузера"; description: "Публичный endpoint. Контексты взаимоисключающие и проверяются сервером. В prod enabled=false без регистрации. UUID обозначает браузерный профиль, не физический телефон."; security : {}; }; } rpc GetDevices(GetDevicesReq) returns (GetDevicesRsp) { option (google.api.http) = { get: "/api/devices" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Устройства"; summary : "Получить устройства площадки"; description: "Только venue; организатор, администратор или разработчик. Связь актуальна менее 60 секунд; подключения удаляются через 15 минут. Фильтр gameID оставляет только подключения указанной игры."; }; } rpc GetTasks(GetTasksReq) returns (GetTasksRsp) { option (google.api.http) = { get: "/api/tasks" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Задачи"; summary : "Получить общую доску задач"; description: "Доступно разработчикам и администратору."; }; } rpc GetTaskAssignees(GetTaskAssigneesReq) returns (GetTaskAssigneesRsp) { option (google.api.http) = { get: "/api/tasks/assignees" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Задачи"; summary : "Получить разработчиков для назначения"; description: "Доступно разработчикам и администратору."; }; } rpc CreateTask(CreateTaskReq) returns (TaskRsp) { option (google.api.http) = { post: "/api/tasks" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Задачи"; summary : "Создать задачу"; description: "Доступно разработчикам и администратору."; }; } rpc UpdateTask(UpdateTaskReq) returns (TaskRsp) { option (google.api.http) = { put: "/api/tasks/{ID}" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Задачи"; summary : "Редактировать задачу"; description: "Доступно разработчикам и администратору."; }; } rpc SetTaskStatus(SetTaskStatusReq) returns (TaskRsp) { option (google.api.http) = { post: "/api/tasks/{ID}/status" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Задачи"; summary : "Изменить статус задачи"; description: "Доступно разработчикам и администратору."; }; } rpc DeleteTask(DeleteTaskReq) returns (DeleteTaskRsp) { option (google.api.http) = { post: "/api/tasks/{ID}/delete" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Задачи"; summary : "Удалить задачу"; description: "Доступно разработчикам и администратору."; }; } rpc GetParticipation(GetParticipationReq) returns (ParticipationRsp) { option (google.api.http) = { post: "/api/participation/get" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security : {}; tags : "Подтверждение участия"; summary : "Получить состояние участия"; description: "Возвращает команду, игру и текущее решение по персональному токену из письма. Авторизация JWT не требуется. Чтение не меняет статус участия. Токен передаётся в теле запроса. Недействительная ссылка и ошибки обработки возвращаются стандартным gRPC status error."; }; } rpc AnswerParticipation(AnswerParticipationReq) returns (ParticipationRsp) { option (google.api.http) = { post: "/api/participation/answer" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security : {}; tags : "Подтверждение участия"; summary : "Подтвердить или отменить участие"; description: "Принимает окончательный ответ капитана: confirmed или cancelled. Авторизация JWT не требуется; доступ определяется персональным токеном. Первый ответ сохраняется, повтор того же ответа успешен, противоположный ответ отклоняется. Новый ответ принимается только до планового начала игры и пока игра имеет статус draft. Ошибки ссылки, значения answer, закрытого приёма или изменения решения возвращаются стандартным gRPC status error; при уже принятом решении возвращается его текущее состояние."; }; } rpc Ping(PingReq) returns (PingRsp) { option (google.api.http) = { get: "/api/test/ping" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } summary: "Проверить доступность"; }; } rpc Echo(EchoReq) returns (EchoRsp) { option (google.api.http) = { post: "/api/test/echo" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } summary: "Проверить обработку данных"; }; } rpc Signup(SignupReq) returns (SignupRsp) { option (google.api.http) = { post: "/api/auth/signup" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } tags : "Регистрация и вход"; summary: "Зарегистрироваться"; }; } rpc RefreshPassword(RefreshPasswordReq) returns (RefreshPasswordRsp) { option (google.api.http) = { post: "/api/auth/refresh-password" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } tags : "Регистрация и вход"; summary: "Обновить пароль"; }; } rpc Login(LoginReq) returns (LoginRsp) { option (google.api.http) = { post: "/api/auth/login" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } tags : "Регистрация и вход"; summary: "Войти и получить токена доступа"; }; } rpc Refresh(RefreshReq) returns (RefreshRsp) { option (google.api.http) = { post: "/api/auth/refresh" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } tags : "Регистрация и вход"; summary: "Обновить токена доступа"; }; } rpc DeleteAccount(DeleteAccountReq) returns (DeleteAccountRsp) { option (google.api.http) = { delete: "/api/auth/delete-account" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Регистрация и вход"; summary : "Удалить учётную запись и персональные данные"; description: "Удаляет учётную запись и персональные данные текущего пользователя (ст. 21 152-ФЗ). Требует заголовок X-Password с паролем учётной записи (подтверждение владельца; пароль не передаётся в URL)."; }; } rpc GetTerms(GetTermsReq) returns (GetTermsRsp) { option (google.api.http) = { get: "/api/terms" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Документы"; summary: "Получить текст Пользовательского соглашения (оферты)"; }; } rpc GetPrivacy(GetPrivacyReq) returns (GetPrivacyRsp) { option (google.api.http) = { get: "/api/privacy" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Документы"; summary: "Получить текст Политики конфиденциальности"; }; } rpc GetConsent(GetConsentReq) returns (GetConsentRsp) { option (google.api.http) = { get: "/api/consent" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Документы"; summary: "Получить текст Согласия на обработку персональных данных"; }; } rpc GetUsers(GetUsersReq) returns (GetUsersRsp) { option (google.api.http) = { get: "/api/users" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Пользователи"; summary: "Получить всех пользователей"; }; } rpc GetUserByID(GetUserByIDReq) returns (GetUserByIDRsp) { option (google.api.http) = { get: "/api/users/{ID}" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Пользователи"; summary: "Получить пользователя по id"; }; } rpc GetMe(GetMeReq) returns (GetMeRsp) { option (google.api.http) = { get: "/api/users/me" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Пользователи"; summary: "Получить информацию о себе"; }; } // Только администратор; сеансы пользователя сохраняются. // Ошибки: 401, 403, 400, 404, 500. rpc ChangeUserPassword(ChangeUserPasswordReq) returns (ChangeUserPasswordRsp) { option (google.api.http) = { post: "/api/users/{ID}/password" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Пользователи"; summary : "Изменить чужой пароль (только администратор)"; description: "Пароль вводится вручную: минимум 12 символов, максимум 72 байта. Сеансы сохраняются. Ошибки: 401 — нет авторизации, 403 — нет прав, 400 — неверные данные или собственный ID, 404 — пользователь не найден, 500 — ошибка сохранения."; }; } rpc AddUserRole(AddUserRoleReq) returns (AddUserRoleRsp) { option (google.api.http) = { post: "/api/users/{ID}/role/add" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Пользователи"; summary: "Добавить пользователю роль"; }; } rpc DeleteUserRole(DeleteUserRoleReq) returns (DeleteUserRoleRsp) { option (google.api.http) = { post: "/api/users/{ID}/role/delete" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Пользователи"; summary: "Забрать у пользователя роль"; }; } rpc GetPermissions(GetPermissionsReq) returns (GetPermissionsRsp) { option (google.api.http) = { get: "/api/ui/permissions" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Доступы"; summary: "Получить список доступных элементов интерфейса"; }; } rpc UploadFile(UploadFileReq) returns (UploadFileRsp) { option (google.api.http) = { post: "/api/files/upload" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Файлы"; summary: "Загрузить файл"; }; } rpc DownloadFile(DownloadFileReq) returns (google.api.HttpBody) { option (google.api.http) = { get: "/api/files/{filename}" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Файлы"; summary: "Получить файл"; }; } rpc GetPlaylists(GetPlaylistsReq) returns (GetPlaylistsRsp) { option (google.api.http) = { get: "/api/playlists" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Музыка"; summary: "Получить плейлисты"; }; } rpc CreatePlaylist(CreatePlaylistReq) returns (CreatePlaylistRsp) { option (google.api.http) = { post: "/api/playlists" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Музыка"; summary: "Создать плейлист"; }; } rpc UpdatePlaylist(UpdatePlaylistReq) returns (UpdatePlaylistRsp) { option (google.api.http) = { put: "/api/playlists/{ID}" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Музыка"; summary: "Изменить плейлист"; }; } rpc DeletePlaylist(DeletePlaylistReq) returns (DeletePlaylistRsp) { option (google.api.http) = { delete: "/api/playlists/{ID}" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Музыка"; summary: "Удалить плейлист"; }; } rpc UploadPlaylistTrack(UploadPlaylistTrackReq) returns (UploadPlaylistTrackRsp) { option (google.api.http) = { post: "/api/playlists/{playlistID}/tracks" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Музыка"; summary: "Загрузить MP3 в плейлист"; }; } rpc DeletePlaylistTrack(DeletePlaylistTrackReq) returns (DeletePlaylistTrackRsp) { option (google.api.http) = { delete: "/api/playlists/{playlistID}/tracks/{trackID}" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Музыка"; summary: "Удалить MP3 из плейлиста"; }; } rpc SetScenarioPlaylist(SetScenarioPlaylistReq) returns (SetScenarioPlaylistRsp) { option (google.api.http) = { put: "/api/scenarios/{ID}/playlist" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Музыка"; summary: "Назначить плейлист сценарию"; }; } rpc AddScenario(AddScenarioReq) returns (AddScenarioRsp) { option (google.api.http) = { post: "/api/scenario" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Сценарии"; summary: "Создать сценарий"; }; } rpc GetMyScenarios(GetMyScenariosReq) returns (GetMyScenariosRsp) { option (google.api.http) = { get: "/api/my-scenarios" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Сценарии"; summary: "Получить свои сценарии"; }; } rpc GetScenariosCatalog(GetScenariosCatalogReq) returns (GetScenariosCatalogRsp) { option (google.api.http) = { get: "/api/catalog" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Сценарии"; summary: "Получить каталог сценариев"; }; } rpc GetFullScenario(GetScenarioReq) returns (GetScenarioRsp) { option (google.api.http) = { get: "/api/scenarios/{ID}/full" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Сценарии"; summary: "Получить сценарий со всеми деталями по id"; }; } rpc GetScenario(GetScenarioReq) returns (GetScenarioRsp) { option (google.api.http) = { get: "/api/scenarios/{ID}" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Сценарии"; summary: "Получить сценарий по id"; }; } rpc UpdateScenario(UpdateScenarioReq) returns (UpdateScenarioRsp) { option (google.api.http) = { put : "/api/scenarios/{ID}" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Сценарии"; summary: "Редактировать сценарий"; }; } rpc PublicScenario(PublicScenarioReq) returns (PublicScenarioRsp) { option (google.api.http) = { put : "/api/scenarios/{ID}/public" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Сценарии"; summary: "Опубликовать сценарий"; }; } rpc DraftScenario(DraftScenarioReq) returns (DraftScenarioRsp) { option (google.api.http) = { put : "/api/scenarios/{ID}/draft" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Сценарии"; summary: "Снять с публикации сценарий"; }; } rpc DeleteScenario(DeleteScenarioReq) returns (DeleteScenarioRsp) { option (google.api.http) = { delete: "/api/scenarios/{ID}" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Сценарии"; summary: "Удалить сценарий"; }; } rpc AddScenarioPlace(AddScenarioPlaceReq) returns (AddScenarioPlaceRsp) { option (google.api.http) = { post: "/api/scenarios/{ID}/places" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Точки сценария"; summary: "Создать точку сценария"; }; } rpc UpdateScenarioPlace(UpdateScenarioPlaceReq) returns (UpdateScenarioPlaceRsp) { option (google.api.http) = { put : "/api/scenarios/{ID}/places/{code}" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Точки сценария"; summary: "Обновить точку сценария"; }; } rpc DeleteScenarioPlace(DeleteScenarioPlaceReq) returns (DeleteScenarioPlaceRsp) { option (google.api.http) = { delete: "/api/scenarios/{ID}/places/{code}" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Точки сценария"; summary: "Удалить точку сценария"; }; } rpc AddScenarioQuestion(AddScenarioQuestionReq) returns (AddScenarioQuestionRsp) { option (google.api.http) = { post: "/api/scenarios/{ID}/questions" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Вопросы сценария"; summary: "Создать вопрос дела"; }; } rpc UpdateScenarioQuestion(UpdateScenarioQuestionReq) returns (UpdateScenarioQuestionRsp) { option (google.api.http) = { put : "/api/scenarios/{ID}/questions/{code}" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Вопросы сценария"; summary: "Обновить вопрос дела"; }; } rpc DeleteScenarioQuestion(DeleteScenarioQuestionReq) returns (DeleteScenarioQuestionRsp) { option (google.api.http) = { delete: "/api/scenarios/{ID}/questions/{code}" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Вопросы сценария"; summary: "Удалить вопрос дела"; }; } // Загрузить файл через UploadFile, затем передать его имя; пустое имя удаляет фон дела. rpc UpdateScenarioBackground(UpdateScenarioBackgroundReq) returns (UpdateScenarioBackgroundRsp) { option (google.api.http) = { put : "/api/scenarios/{ID}/background" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Сценарии"; summary: "Установить или удалить фон дела"; }; } rpc UpdateScenarioIntro(UpdateScenarioIntroReq) returns (UpdateScenarioIntroRsp) { option (google.api.http) = { put : "/api/scenarios/{ID}/introduction" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Сценарии"; summary: "Обновить введение сценария"; }; } // Видео загружается через UploadFile (MP4/WebM). Пустая conclusion удаляет закрывашку. rpc UpdateScenarioConclusion(UpdateScenarioConclusionReq) returns (UpdateScenarioConclusionRsp) { option (google.api.http) = { put : "/api/scenarios/{ID}/conclusion" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Сценарии"; summary: "Обновить закрывашку сценария (игрокам не передаётся)"; }; } rpc DownloadScenarioArchive(DownloadScenarioArchiveReq) returns (google.api.HttpBody) { option (google.api.http) = { get: "/api/scenarios/{ID}/archive" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Сценарии"; summary: "Скачать сценарий архивом (со всеми материалами и картинками)"; }; } rpc UploadScenarioArchive(google.api.HttpBody) returns (UploadScenarioArchiveRsp) { option (google.api.http) = { post: "/api/scenarios/archive" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Сценарии"; summary: "Создать сценарий из архива"; }; } rpc AddGame(AddGameReq) returns (AddGameRsp) { option (google.api.http) = { post: "/api/games" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Игры"; summary: "Создать игру"; }; } rpc GetGames(GetGamesReq) returns (GetGamesRsp) { option (google.api.http) = { get: "/api/games" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Игры"; summary: "Получить игры"; }; } rpc GetGame(GetGameReq) returns (GetGameRsp) { option (google.api.http) = { get: "/api/games/{ID}" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Игры"; summary: "Получить игру"; }; } // HTTP uploads are streamed by a dedicated handler, bypassing gRPC message limits. rpc ImportGame(google.api.HttpBody) returns (GameImportRsp) { option (google.api.http) = { post: "/api/games/import" body: "*" }; } // Dedicated HTTP handlers stream ZIPs; gRPC retains its message size limit. rpc DownloadGameResultsPackage(GameResultsPackageReq) returns (google.api.HttpBody) { option (google.api.http) = {get: "/api/games/{ID}/results-package/download"}; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Игры"; summary : "Скачать свежий ZIP результатов завершённой venue-игры"; produces: "application/zip"; }; } rpc ApplyGameResultsPackage(ApplyGameResultsPackageReq) returns (GameResultsPackageRsp) { option (google.api.http) = { post: "/api/games/{ID}/results-package/apply" body: "package" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Игры"; summary : "Применить ZIP результатов на исходной prod-игре"; description: "HTTP принимает сырой ZIP до 1 ГиБ, включая поток без Content-Length. Ошибки: 400 неверный пакет/игра, 413 размер, 409 конфликт, 503 временный отказ."; consumes : "application/zip"; produces : "application/json"; }; } rpc StartGameExport(StartGameExportReq) returns (GameExportRsp) { option (google.api.http) = { post: "/api/games/{ID}/export" body: "*" }; } rpc GetGameExport(GetGameExportReq) returns (GameExportRsp) { option (google.api.http) = {get: "/api/games/{ID}/export"}; } rpc IssueGameTickets(IssueGameTicketsReq) returns (IssueGameTicketsRsp) { option (google.api.http) = { post: "/api/games/{gameID}/tickets" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } tags : "Билеты"; summary : "Выпустить билеты на игру"; }; } rpc GetTicket(GetTicketReq) returns (GetTicketRsp) { option (google.api.http) = { get: "/api/tickets/{token}" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } tags : "Билеты"; summary : "Получить данные билета"; }; } rpc AcceptTicket(AcceptTicketReq) returns (AcceptTicketRsp) { option (google.api.http) = { post: "/api/tickets/{token}/accept" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Билеты"; summary: "Принять билет"; }; } // Создать сессию экрана. Экран получает токен и показывает QR для привязки. rpc CreateScreenSession(CreateScreenSessionReq) returns (CreateScreenSessionRsp) { option (google.api.http) = { post: "/api/screens" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } tags : "Экраны"; summary: "Создать сессию экрана"; }; } // Получить состояние экрана по его токену. Вызывается самим экраном. rpc GetScreenState(GetScreenStateReq) returns (GetScreenStateRsp) { option (google.api.http) = { post: "/api/screens/state" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } tags : "Экраны"; summary: "Получить состояние экрана"; }; } rpc GetGameScreens(GetGameScreensReq) returns (GetGameScreensRsp) { option (google.api.http) = { get: "/api/games/{gameID}/screens" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Экраны"; summary: "Получить экраны игры"; }; } rpc GetGameMusic(GetGameMusicReq) returns (GetGameMusicRsp) { option (google.api.http) = { get: "/api/games/{gameID}/music" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Музыка"; summary: "Получить музыку игры"; }; } rpc SetGameMusic(SetGameMusicReq) returns (SetGameMusicRsp) { option (google.api.http) = { post: "/api/games/{gameID}/music" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Музыка"; summary: "Изменить музыку игры"; }; } rpc AdvanceScreenMusic(AdvanceScreenMusicReq) returns (AdvanceScreenMusicRsp) { option (google.api.http) = { post: "/api/screens/music/advance" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } tags : "Музыка"; summary : "Перейти к следующему треку экрана"; }; } // Привязать экран к игре. Требует авторизацию организатора, автора или администратора. rpc BindScreen(BindScreenReq) returns (BindScreenRsp) { option (google.api.http) = { post: "/api/screens/{screenID}/bind" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Экраны"; summary: "Привязать экран к игре"; }; } // Отвязать экран от игры. Требует авторизацию организатора, автора или администратора. rpc UnbindScreen(UnbindScreenReq) returns (UnbindScreenRsp) { option (google.api.http) = { post: "/api/screens/{screenID}/unbind" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Экраны"; summary: "Отвязать экран от игры"; }; } rpc SetScreenSound(SetScreenSoundReq) returns (SetScreenSoundRsp) { option (google.api.http) = { post: "/api/screens/{screenID}/sound" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Экраны"; summary: "Назначить экран со звуком"; }; } rpc GetGameMap(GetGameMapReq) returns (GetGameMapRsp) { option (google.api.http) = { get: "/api/games/{gameID}/map" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Экраны"; summary: "Получить карту игры"; }; } rpc UpdateGameMapItem(UpdateGameMapItemReq) returns (UpdateGameMapItemRsp) { option (google.api.http) = { post: "/api/games/{gameID}/map/items/{kind}/{itemID}" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Экраны"; summary: "Изменить объект карты игры"; }; } // Изменить общий режим показа всех экранов игры. rpc SetScreenMode(SetScreenModeReq) returns (SetScreenModeRsp) { option (google.api.http) = { post: "/api/games/{gameID}/screens/mode" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Экраны"; summary: "Изменить режим показа экранов"; }; } rpc SetScreenTimer(SetScreenTimerReq) returns (SetScreenTimerRsp) { option (google.api.http) = { post: "/api/games/{gameID}/screens/timer" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Экраны"; summary: "Настроить таймер экранов"; }; } // Отправить общую команду экранам игры. rpc SendScreenCommand(SendScreenCommandReq) returns (SendScreenCommandRsp) { option (google.api.http) = { post: "/api/games/{gameID}/screens/command" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Экраны"; summary: "Отправить команду экранам"; }; } rpc GetScreenPlan(GetScreenPlanReq) returns (GetScreenPlanRsp) { option (google.api.http) = { get: "/api/games/{gameID}/screen-plan" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Экраны"; summary: "Получить план экранов"; }; } rpc SaveScreenPlan(SaveScreenPlanReq) returns (SaveScreenPlanRsp) { option (google.api.http) = { put : "/api/games/{gameID}/screen-plan" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Экраны"; summary: "Сохранить план экранов"; }; } rpc SetScreenPlanStep(SetScreenPlanStepReq) returns (SetScreenPlanStepRsp) { option (google.api.http) = { post: "/api/games/{gameID}/screen-plan/step" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Экраны"; summary: "Перейти на шаг плана экранов"; }; } rpc GetScreenResults(GetScreenResultsReq) returns (GetScreenResultsRsp) { option (google.api.http) = { post: "/api/screens/results" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Экраны"; summary: "Получить результаты для экрана"; }; } 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" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Игры"; summary: "Получить ответы команд на вопросы дела (для оценки организатором)"; }; } rpc GetGameResults(GetGameResultsReq) returns (GetGameResultsRsp) { option (google.api.http) = { get: "/api/games/{gameID}/results" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Игры"; summary: "Получить итоговую таблицу игры (после завершения)"; }; } rpc CreateGameReview(CreateGameReviewReq) returns (CreateGameReviewRsp) { option (google.api.http) = { post: "/api/games/{gameID}/reviews" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } tags : "Отзывы" summary : "Оставить отзыв об игре" }; } rpc GetGameReviews(GetGameReviewsReq) returns (GetGameReviewsRsp) { option (google.api.http) = { get: "/api/games/{gameID}/reviews" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } tags : "Отзывы" summary : "Получить отзывы об игре" }; } rpc GetScenarioReviews(GetScenarioReviewsReq) returns (GetGameReviewsRsp) { option (google.api.http) = { get: "/api/scenarios/{scenarioID}/reviews" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } tags : "Отзывы" summary : "Получить отзывы сценария" }; } rpc GetGameReviewsForModeration(GetGameReviewsReq) returns (GetGameReviewsRsp) { option (google.api.http) = { get: "/api/admin/games/{gameID}/reviews" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Отзывы" summary: "Получить отзывы игры для модерации" }; } rpc GetReviewsForModeration(GetReviewsForModerationReq) returns (GetGameReviewsRsp) { option (google.api.http) = { get: "/api/admin/game-reviews" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Отзывы" summary: "Получить общую очередь отзывов" }; } rpc ModerateGameReview(ModerateGameReviewReq) returns (ModerateGameReviewRsp) { option (google.api.http) = { put : "/api/admin/game-reviews/{ID}" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Отзывы" summary: "Модерировать отзыв" }; } rpc UpdateGame(UpdateGameReq) returns (UpdateGameRsp) { option (google.api.http) = { put : "/api/games/{ID}" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Игры"; summary: "Обновить игру"; }; } rpc StartGame(StartGameReq) returns (StartGameRsp) { option (google.api.http) = { put : "/api/games/{ID}/start" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Игры"; summary: "Начать игру"; }; } rpc PauseGame(PauseGameReq) returns (PauseGameRsp) { option (google.api.http) = { put : "/api/games/{ID}/pause" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Игры"; summary: "Остановить игру"; }; } rpc PlayGame(PlayGameReq) returns (PlayGameRsp) { option (google.api.http) = { put : "/api/games/{ID}/play" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Игры"; summary: "Продолжить игру"; }; } rpc FinishGame(FinishGameReq) returns (FinishGameRsp) { option (google.api.http) = { put : "/api/games/{ID}/finish" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Игры"; summary: "Закончить игру"; }; } rpc ResetGame(ResetGameReq) returns (ResetGameRsp) { option (google.api.http) = { put : "/api/games/{ID}/reset" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Игры"; summary: "Сбросить игру"; }; } rpc DeleteGame(DeleteGameReq) returns (DeleteGameRsp) { option (google.api.http) = { delete: "/api/games/{ID}" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Игры"; summary: "Удалить игру"; }; } rpc AddTeam(AddTeamReq) returns (AddTeamRsp) { option (google.api.http) = { post: "/api/teams", body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Команда"; summary: "Создать команду"; }; } rpc UpdateTeam(UpdateTeamReq) returns (UpdateTeamRsp) { option (google.api.http) = { put : "/api/teams/{ID}", body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Команда"; summary: "Обновить команду"; }; } rpc DeleteTeam(DeleteTeamReq) returns (DeleteTeamRsp) { option (google.api.http) = { delete: "/api/teams/{ID}" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Команда"; summary: "Удалить команду"; }; } rpc GiveTeamApplications(GiveTeamApplicationsReq) returns (GiveTeamApplicationsRsp) { option (google.api.http) = { post: "/api/teams/{ID}/applications", body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Команда"; summary: "Выдать улику"; }; } rpc GetTeamStory(GetTeamStoryReq) returns (GetTeamStoryRsp) { option (google.api.http) = { get: "/api/teams/{ID}/story" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } tags : "Ходы"; summary: "Получить ходы"; }; } rpc AddTeamAction(AddTeamActionReq) returns (AddTeamActionRsp) { option (google.api.http) = { post: "/api/teams/{ID}/actions", body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } tags : "Ходы"; summary: "Сделать ход"; }; } rpc DeleteLastTeamAction(DeleteLastTeamActionReq) returns (DeleteLastTeamActionRsp) { option (google.api.http) = { delete: "/api/teams/{ID}/last-actions" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Ходы"; summary: "Удалить последний ход"; }; } rpc GetTeamAnswers(GetTeamAnswersReq) returns (GetTeamAnswersRsp) { option (google.api.http) = { get: "/api/teams/{ID}/answers" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } tags : "Ходы"; summary: "Получить ответы команды на вопросы дела"; }; } rpc SubmitTeamAnswers(SubmitTeamAnswersReq) returns (SubmitTeamAnswersRsp) { option (google.api.http) = { post: "/api/teams/{ID}/answers" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { security: { security_requirement: {} } tags : "Ходы"; summary: "Отправить ответы команды на вопросы дела"; }; } rpc SetTeamAnswerScore(SetTeamAnswerScoreReq) returns (SetTeamAnswerScoreRsp) { option (google.api.http) = { put : "/api/teams/{ID}/answers/{questionCode}/score" body: "*" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { tags : "Игры"; summary: "Выставить балл команде за ответ на вопрос дела"; }; } } message PingReq {} message PingRsp {} message EchoReq { string text = 1; } message EchoRsp { string text = 1; } message SignupReq { string username = 1; string email = 2; bool accept_terms = 3; bool accept_privacy = 4; } message SignupRsp { } message RefreshPasswordReq { string email = 1; } message RefreshPasswordRsp { } message LoginReq { string email = 1; string password = 2; } message LoginRsp { string accessToken = 1; string refreshToken = 2; } message RefreshReq { string refreshToken = 1; } message RefreshRsp { string accessToken = 1; string refreshToken = 2; } // Пароль подтверждает, что удаляет аккаунт его владелец, а не обладатель // украденного access-токена (ст. 21 152-ФЗ). Передаётся HTTP-заголовком // X-Password (пробрасывается матчером в cmd/evening_detective_server/main.go), // чтобы креденшел не попадал в URL/query-параметры и логи. message DeleteAccountReq {} message DeleteAccountRsp { } // Документы раздаются по постоянным URL, на которые ссылается сервис при // фиксации акцептов соглашений (user_agreements) и форма регистрации. message GetTermsReq {} message GetTermsRsp { string text = 1; } message GetPrivacyReq {} message GetPrivacyRsp { string text = 1; } message GetConsentReq {} message GetConsentRsp { string text = 1; } message GetUsersReq {} message GetUsersRsp { repeated User users = 1; } message User { int32 ID = 1; string username = 2; string email = 3; repeated string roles = 4; bool is_active = 5; google.protobuf.Timestamp created_at = 6; } message GetUserByIDReq { int32 ID = 1; } message GetUserByIDRsp { User user = 1; } message GetMeReq {} message GetMeRsp { User user = 1; } message ChangeUserPasswordReq { int32 ID = 1; string new_password = 2; } message ChangeUserPasswordRsp {} message AddUserRoleReq { int32 ID = 1; string role = 2; } message AddUserRoleRsp { } message DeleteUserRoleReq { int32 ID = 1; string role = 2; } message DeleteUserRoleRsp { } message GetPermissionsReq {} message GetPermissionsRsp { repeated string permissions = 1; // Actual server environment: prod or venue. string environment = 2; } message UploadFileReq { string filename = 1; bytes data = 2; } message UploadFileRsp { string filename = 1; // Тип загруженного файла: image | pdf | audio | video (определяется сервером по // содержимому). Удобен редактору для предзаполнения file_type улики. string file_type = 2 [json_name = "file_type"]; } message DownloadFileReq { string filename = 1; } message PlaylistTrack { int32 ID = 1; string name = 2; string filename = 3; int32 position = 4; } message Playlist { int32 ID = 1; string name = 2; repeated PlaylistTrack tracks = 3; } message GetPlaylistsReq {} message GetPlaylistsRsp { repeated Playlist playlists = 1; } message CreatePlaylistReq { string name = 1; } message CreatePlaylistRsp { Playlist playlist = 1; } message UpdatePlaylistReq { int32 ID = 1; string name = 2; } message UpdatePlaylistRsp { Playlist playlist = 1; } message DeletePlaylistReq { int32 ID = 1; } message DeletePlaylistRsp {} message UploadPlaylistTrackReq { int32 playlistID = 1; string name = 2; string filename = 3; bytes data = 4; } message UploadPlaylistTrackRsp { Playlist playlist = 1; } message DeletePlaylistTrackReq { int32 playlistID = 1; int32 trackID = 2; } message DeletePlaylistTrackRsp {} message SetScenarioPlaylistReq { int32 ID = 1; int32 playlistID = 2; } message SetScenarioPlaylistRsp {} message AddScenarioReq { string name = 1; } message AddScenarioRsp { int32 ID = 1; } message GetMyScenariosReq {} message GetMyScenariosRsp { repeated Scenario scenarios = 1; } message GetScenariosCatalogReq {} message GetScenariosCatalogRsp { repeated Scenario scenarios = 1; } message GetScenarioReq { int32 ID = 1; } message GetScenarioRsp { Scenario scenario = 1; } message Scenario { int32 ID = 1; string name = 2; optional string description = 3; optional string image = 4; Story story = 5; string status = 6; User author = 7; google.protobuf.Timestamp updated_at = 8; google.protobuf.Timestamp created_at = 9; google.protobuf.Timestamp published_at = 10; double average_rating = 11; int32 reviews_count = 12; string difficulty = 13; optional int32 music_playlist_id = 14 [json_name = "music_playlist_id"]; } message Story { repeated Place places = 1; Introduction introduction = 2; repeated Question questions = 3; string background = 4; Conclusion conclusion = 5; // Только в полном сценарии, игрокам не передаётся. } message Conclusion { string text = 1; string video = 2; } message Introduction { string text = 1; string audio = 2; } message Place { string code = 1; string name = 2; string text = 3; string image = 4; bool hidden = 5; repeated Application applications = 6; repeated Door doors = 7; repeated Key keys = 8; } message Application { string name = 1; string image = 2; // Тип файла улики: image | pdf | audio. Значение выводится сервером из // расширения файла (см. file_storage.FileType); пустое значение возможно // только для legacy-строк без расширения. string file_type = 3 [json_name = "file_type"]; } message Door { string code = 1; string name = 2; repeated Key keys = 3; } message Key { string name = 1; } // Вопрос дела - вопрос, на который команда должна ответить в конце игры. // Правильный ответ (answer) виден только автору сценария и организаторам // (при оценке ответов команд), команде не передаётся. message Question { // Код вопроса - используется при отправке ответов командой string code = 1; // Текст вопроса string text = 2; // Правильный ответ (справка для организатора при оценке) string answer = 3; } // Ответ команды на вопрос дела message Answer { // Код вопроса string questionCode = 1; // Текст ответа команды string answer = 2; } // Ответ команды с баллом (для панели организатора) message AnswerScore { string questionCode = 1; string answer = 2; // Балл, выставленный организатором; отсутствует, если ещё не оценено optional int32 score = 3; } // Ответы одной команды на вопросы дела message TeamAnswers { Team team = 1; repeated AnswerScore answers = 2; } message UpdateScenarioReq { int32 ID = 1; string name = 2; string description = 3; string image = 4; } message UpdateScenarioRsp { } message PublicScenarioReq { int32 ID = 1; } message PublicScenarioRsp { } message DraftScenarioReq { int32 ID = 1; } message DraftScenarioRsp { } message DeleteScenarioReq { int32 ID = 1; } message DeleteScenarioRsp { } message AddScenarioPlaceReq { int32 ID = 1; Place place = 2; } message AddScenarioPlaceRsp { } message UpdateScenarioPlaceReq { int32 ID = 1; string code = 2; Place place = 3; } message UpdateScenarioPlaceRsp { } message DeleteScenarioPlaceReq { int32 ID = 1; string code = 2; } message DeleteScenarioPlaceRsp { } message UpdateScenarioConclusionReq { int32 ID = 1; Conclusion conclusion = 2; } message UpdateScenarioConclusionRsp { } message UpdateScenarioIntroReq { int32 ID = 1; Introduction introduction = 2; } message UpdateScenarioIntroRsp { } message AddScenarioQuestionReq { int32 ID = 1; Question question = 2; } message AddScenarioQuestionRsp { } message UpdateScenarioQuestionReq { int32 ID = 1; string code = 2; Question question = 3; } message UpdateScenarioQuestionRsp { } message DeleteScenarioQuestionReq { int32 ID = 1; string code = 2; } message DeleteScenarioQuestionRsp { } message DownloadScenarioArchiveReq { int32 ID = 1; } message UploadScenarioArchiveRsp { int32 ID = 1; } message AddGameReq { string name = 1; string description = 2; google.protobuf.Timestamp start_at = 3; int32 scenarioID = 4; string format = 5; string location = 6; } message AddGameRsp { int32 ID = 1; } message GetGamesReq {} message GetGamesRsp { repeated Game games = 1; } message Game { int32 ID = 1; string name = 2; string description = 3; google.protobuf.Timestamp start_at = 4; string status = 5; Scenario scenario = 6; repeated Team teams = 7; optional google.protobuf.Timestamp started_at = 8; optional google.protobuf.Timestamp ended_at = 9; string format = 10; string location = 11; GameExport export = 12; } // Download ready ZIP through the authenticated streaming HTTP route: // GET /api/games/{ID}/export/download (not an HttpBody RPC). message GameExport { string ID = 1; string state = 2; // preparing, ready or failed; preparing/ready lock game writes. string packageID = 3; int64 size = 4; string sha256 = 5; string failure = 6; } message GameImportRsp { int32 gameID = 1; string packageID = 2; bool repeated = 3; } message StartGameExportReq { int32 ID = 1; } message GetGameExportReq { int32 ID = 1; } message GameExportRsp { GameExport export = 1; } enum ScreenMode { SCREEN_MODE_UNSPECIFIED = 0; SCREEN_MODE_COVER = 1; SCREEN_MODE_INTRO = 2; SCREEN_MODE_INTRO_DONE = 3; SCREEN_MODE_CONCLUSION = 4; SCREEN_MODE_CONCLUSION_DONE = 5; SCREEN_MODE_RESULTS = 6; SCREEN_MODE_TIMER = 7; SCREEN_MODE_TEXT = 8; } enum ScreenTimerState { SCREEN_TIMER_STATE_UNSPECIFIED = 0; SCREEN_TIMER_STATE_STOPPED = 1; SCREEN_TIMER_STATE_RUNNING = 2; SCREEN_TIMER_STATE_PAUSED = 3; SCREEN_TIMER_STATE_FINISHED = 4; } enum ScreenCommand { SCREEN_COMMAND_UNSPECIFIED = 0; SCREEN_COMMAND_PLAY = 1; SCREEN_COMMAND_PAUSE = 2; SCREEN_COMMAND_RESUME = 3; SCREEN_COMMAND_STOP = 4; SCREEN_COMMAND_REPLAY = 5; SCREEN_COMMAND_SEEK = 6; SCREEN_COMMAND_VOLUME = 7; SCREEN_COMMAND_TIMER_START = 8; SCREEN_COMMAND_TIMER_PAUSE = 9; SCREEN_COMMAND_TIMER_RESUME = 10; SCREEN_COMMAND_TIMER_RESET = 11; } message ScreenState { string ID = 1; string status = 2; optional int32 gameID = 3; ScreenMode mode = 4; bool sound_enabled = 5; string error = 6; int64 state_version = 7; int64 position_ms = 8; string game_name = 9; string cover_image = 10; string intro_audio = 11; string conclusion_video = 12; ScreenCommand command = 13; int32 volume = 14; string background_image = 15; string timer_label = 16; int64 timer_duration_seconds = 17; int64 timer_remaining_seconds = 18; ScreenTimerState timer_state = 19; string text = 20; MusicState music = 21; } message CreateScreenSessionReq {} message CreateScreenSessionRsp { string screenID = 1; string access_token = 2; } message GetScreenStateReq { string screenID = 1; string accessToken = 2; } message GetScreenStateRsp { ScreenState state = 1; } message GetGameScreensReq { int32 gameID = 1; } message GetGameScreensRsp { repeated ScreenState screens = 1; } enum MusicPlaybackState { MUSIC_PLAYBACK_UNSPECIFIED = 0; MUSIC_PLAYBACK_STOPPED = 1; MUSIC_PLAYBACK_PLAYING = 2; MUSIC_PLAYBACK_PAUSED = 3; } message MusicState { Playlist playlist = 1; int32 track_index = 2; int64 position_ms = 3; int32 volume = 4; bool loop = 5; MusicPlaybackState playback = 6; int64 state_version = 7; } message GetGameMusicReq { int32 gameID = 1; } message GetGameMusicRsp { repeated Playlist playlists = 1; MusicState state = 2; int32 scenario_playlist_id = 3; int32 game_playlist_id = 4; } message SetGameMusicReq { int32 gameID = 1; int32 playlistID = 2; int32 track_index = 3; int64 position_ms = 4; int32 volume = 5; bool loop = 6; MusicPlaybackState playback = 7; } message SetGameMusicRsp { MusicState state = 1; } message AdvanceScreenMusicReq { string screenID = 1; string accessToken = 2; } message AdvanceScreenMusicRsp { MusicState state = 1; } message BindScreenReq { string screenID = 1; string accessToken = 2; int32 gameID = 3; } message BindScreenRsp { ScreenState state = 1; } message UnbindScreenReq { string screenID = 1; } message UnbindScreenRsp { } message SetScreenSoundReq { string screenID = 1; bool soundEnabled = 2; } message SetScreenSoundRsp { } message MapItem { string kind = 1; int32 itemID = 2; string label = 3; int32 x = 4; int32 y = 5; bool hidden = 6; string status = 7; bool sound_enabled = 8; } message GetGameMapReq { int32 gameID = 1; } message GetGameMapRsp { repeated MapItem items = 1; } message UpdateGameMapItemReq { int32 gameID = 1; string kind = 2; int32 itemID = 3; int32 x = 4; int32 y = 5; bool hidden = 6; } message UpdateGameMapItemRsp {} message SetScreenModeReq { int32 gameID = 1; ScreenMode mode = 2; } message SetScreenModeRsp { } message SetScreenTimerReq { int32 gameID = 1; int64 duration_seconds = 2; string label = 3; } message SetScreenTimerRsp { } message SendScreenCommandReq { int32 gameID = 1; ScreenCommand command = 2; int64 position_ms = 3; int32 volume = 4; } message SendScreenCommandRsp { } message ScreenPlanStep { ScreenMode mode = 1; string text = 2; string timer_label = 3; int64 timer_duration_seconds = 4; } message ScreenPlan { repeated ScreenPlanStep steps = 1; int32 current_step = 2; } message GetScreenPlanReq { int32 gameID = 1; } message GetScreenPlanRsp { ScreenPlan plan = 1; } message SaveScreenPlanReq { int32 gameID = 1; repeated ScreenPlanStep steps = 2; int32 current_step = 3; } message SaveScreenPlanRsp { ScreenPlan plan = 1; } message SetScreenPlanStepReq { int32 gameID = 1; int32 current_step = 2; } message SetScreenPlanStepRsp { ScreenPlan plan = 1; } message GetScreenResultsReq { string screenID = 1; string accessToken = 2; } message GetScreenResultsRsp { repeated ResultsQuestion questions = 1; repeated TeamResult teams = 2; } message GetParticipationReq { // Персональный токен из письма: 64 шестнадцатеричных символа. Не является JWT. string token = 1; } message AnswerParticipationReq { // Персональный токен из письма: 64 шестнадцатеричных символа. Не является JWT. string token = 1; // Окончательное решение: confirmed — участвуем, cancelled — отменяем запись. string answer = 2; } message ParticipationRsp { // Название команды. Данные доступны только по действительному токену. string team_name = 1; // Название игры. string game_name = 2; // Плановое начало игры, UTC (RFC 3339 в JSON). google.protobuf.Timestamp start_at = 3; // Заявленное число участников команды. int32 participants_count = 4; // pending — ожидает ответа, confirmed — подтверждено, cancelled — отменено. string status = 5; // Новые ответы закрыты: наступило плановое начало или игра вышла из draft. // Изменение уже принятого решения запрещено независимо от этого поля. bool closed = 6; } message Team { 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; // Статус участия: pending, confirmed или cancelled. Поле панели организатора. string participation_status = 9; // Источник решения: captain, automatic либо пустая строка до ответа. string participation_source = 10; // Последняя ошибка отправки; пустая строка, если ошибки нет. string participation_email_error = 11; // Письмо принято SMTP-сервером; не означает прочтение или доставку во входящие. bool participation_email_sent = 12; } message GetGameReq { int32 ID = 1; } message GetGameRsp { Game game = 1; } message GetGameAnswersReq { int32 gameID = 1; } message GetGameAnswersRsp { repeated Question questions = 1; repeated TeamAnswers teams = 2; } message GetGameResultsReq { int32 gameID = 1; } // Вопрос в шапке итоговой таблицы: правильный ответ (поле answer сообщения // Question) командам/таблице не передаётся — он виден только в панели оценки. message ResultsQuestion { string code = 1; string text = 2; } // Строка итоговой таблицы: команда, баллы по вопросам, сумма и место. message TeamResult { Team team = 1; repeated AnswerScore answers = 2; // Сумма баллов по всем вопросам сценария (неоценённые считаются как 0) int32 total_score = 3; // Место в итоговой таблице: команды с одинаковой суммой баллов и // одинаковым числом ходов занимают одно место (следующее пропускается) int32 place = 4; // Количество ходов команды (дублирует team.actions_count для удобства // таблицы; участвует в тай-брейке при равенстве суммы баллов) int32 actions_count = 5; } message GetGameResultsRsp { repeated ResultsQuestion questions = 1; repeated TeamResult teams = 2; } message GameReview { int32 ID = 1; int32 gameID = 2; int32 teamID = 3; string team_name = 4; int32 rating = 5; string text = 6; string status = 7; google.protobuf.Timestamp created_at = 8; string game_name = 9; int32 difficulty = 10; } message CreateGameReviewReq { int32 gameID = 1; int32 teamID = 2; string password = 3; int32 rating = 4; string text = 5; int32 difficulty = 6; } message CreateGameReviewRsp { GameReview review = 1; } message GetGameReviewsReq { int32 gameID = 1; } message GetScenarioReviewsReq { int32 scenarioID = 1; } message GetReviewsForModerationReq {} message GetGameReviewsRsp { double average_rating = 1; int32 reviews_count = 2; repeated GameReview reviews = 3; } message ModerateGameReviewReq { int32 ID = 1; string status = 2; } message ModerateGameReviewRsp { GameReview review = 1; } message UpdateGameReq { int32 ID = 1; string name = 2; string description = 3; google.protobuf.Timestamp start_at = 4; int32 scenarioID = 5; string format = 6; string location = 7; } message UpdateGameRsp { } message StartGameReq { int32 ID = 1; } message StartGameRsp { } message PauseGameReq { int32 ID = 1; } message PauseGameRsp { } message PlayGameReq { int32 ID = 1; } message PlayGameRsp { } message FinishGameReq { int32 ID = 1; } message FinishGameRsp { } message ResetGameReq { int32 ID = 1; } message ResetGameRsp { } message DeleteGameReq { int32 ID = 1; } message DeleteGameRsp { } message AddTeamReq { int32 gameID = 1; string name = 2; string captain_email = 3; int32 participants_count = 4; } message AddTeamRsp { int32 ID = 1; string password = 2; } message UpdateTeamReq { int32 ID = 1; string name = 2; } message UpdateTeamRsp { } message DeleteTeamReq { int64 ID = 1; } message DeleteTeamRsp { } message GiveTeamApplicationsReq { int64 ID = 1; string name = 2; } message GiveTeamApplicationsRsp { } message GetTeamStoryReq { int64 ID = 1; string password = 2; } message GetTeamStoryRsp { Story story = 1; Game game = 2; repeated TeamAction actions = 3; } message TeamAction { string code = 1; bool mcp = 2; } message AddTeamActionReq { int64 ID = 1; string password = 2; string code = 3; } message AddTeamActionRsp { } message DeleteLastTeamActionReq { int64 ID = 1; } message DeleteLastTeamActionRsp { } message GetTeamAnswersReq { int64 ID = 1; string password = 2; } message GetTeamAnswersRsp { repeated Answer answers = 1; } message SubmitTeamAnswersReq { int64 ID = 1; string password = 2; repeated Answer answers = 3; } message SubmitTeamAnswersRsp { } message SetTeamAnswerScoreReq { int64 ID = 1; string questionCode = 2; // Балл; отсутствует — сбросить выставленный балл optional int32 score = 3; } message SetTeamAnswerScoreRsp { } message UpdateScenarioBackgroundReq { int32 ID = 1; string background = 2; } message UpdateScenarioBackgroundRsp { } message IssueGameTicketsReq { int32 gameID = 1; int32 count = 2; } message IssueGameTicketsRsp { repeated Ticket tickets = 1; } message Ticket { int32 ID = 1; string URL = 2; } message GetTicketReq { string token = 1; } message GetTicketRsp { TicketPage ticket = 1; } message AcceptTicketReq { string token = 1; } message AcceptTicketRsp { TicketPage ticket = 1; } message TicketPage { int32 ID = 1; string game_name = 2; google.protobuf.Timestamp start_at = 3; string location = 4; string status = 5; string game_status = 6; optional google.protobuf.Timestamp used_at = 7; string used_by = 8; } // Developer tasks use new, in_progress, review and done statuses. message DeveloperTask { int32 ID = 1; string title = 2; string description = 3; string status = 4; // Omitted assignee means the task is unassigned. optional int32 assigneeID = 5; string assigneeName = 6; optional int32 authorID = 7; // Calendar deadline in YYYY-MM-DD format; empty means no deadline. string dueDate = 8; google.protobuf.Timestamp createdAt = 9; google.protobuf.Timestamp updatedAt = 10; // Required for updates and deletion to detect concurrent edits. int32 version = 11; } message TaskAssignee { int32 ID = 1; string name = 2; } message GetTasksReq {} message GetTasksRsp { repeated DeveloperTask tasks = 1; } message GetTaskAssigneesReq {} message GetTaskAssigneesRsp { repeated TaskAssignee assignees = 1; } message CreateTaskReq { string title = 1; string description = 2; string status = 3; optional int32 assigneeID = 4; string dueDate = 5; } message UpdateTaskReq { int32 ID = 1; string title = 2; string description = 3; string status = 4; optional int32 assigneeID = 5; string dueDate = 6; int32 version = 7; } message SetTaskStatusReq { int32 ID = 1; string status = 2; int32 version = 3; } message DeleteTaskReq { int32 ID = 1; int32 version = 2; } message TaskRsp { DeveloperTask task = 1; } message DeleteTaskRsp {} message GameResultsPackageReq { int32 ID = 1; } message ApplyGameResultsPackageReq { int32 ID = 1; google.api.HttpBody package = 2; } message GameResultsPackageRsp { int32 gameID = 1; string packageID = 2; string appliedAt = 3; } message DeviceHeartbeatReq { string deviceID = 1; string connectionID = 2; optional int32 organizerGameID = 3; optional int32 teamID = 4; string password = 5; string screenID = 6; string accessToken = 7; } message DeviceHeartbeatRsp { optional bool enabled = 1; google.protobuf.Timestamp serverTime = 2; } message GetDevicesReq { optional int32 gameID = 1; } message GetDevicesRsp { repeated VenueDevice devices = 1; google.protobuf.Timestamp serverTime = 2; } message VenueDevice { string ID = 1; google.protobuf.Timestamp firstSeenAt = 2; google.protobuf.Timestamp lastSeenAt = 3; bool connected = 4; repeated DeviceConnection connections = 5; } message DeviceConnection { string ID = 1; google.protobuf.Timestamp firstSeenAt = 2; google.protobuf.Timestamp lastSeenAt = 3; string IP = 4; string userAgent = 5; bool connected = 6; optional int32 gameID = 7; optional int32 teamID = 8; optional int32 screenID = 9; string kind = 10; // browser, organizer, team, screen }