From 41d3e8ac5e067aff102b243d7c208e10fb309592 Mon Sep 17 00:00:00 2001 From: Fedorov Vladimir Date: Sun, 18 Oct 2026 04:40:55 +0700 Subject: [PATCH] up --- COMPLETED_TASKS.md | 27 + README.md | 25 + electron/api-client.test.ts | 80 + electron/api-values.ts | 44 + .../crabs/evening_detective_server/index.ts | 3742 +++++++++++++++++ electron/online.test.ts | 8 +- electron/online.ts | 247 +- electron/screen-package.ts | 21 +- eslint.config.mjs | 11 +- package.json | 3 +- scripts/online-smoke.cjs | 10 +- scripts/proto-gen.cjs | 44 + 12 files changed, 4105 insertions(+), 157 deletions(-) create mode 100644 electron/api-client.test.ts create mode 100644 electron/api-values.ts create mode 100644 electron/api/generated/crabs/evening_detective_server/index.ts create mode 100644 scripts/proto-gen.cjs diff --git a/COMPLETED_TASKS.md b/COMPLETED_TASKS.md index af33a18..4b17333 100644 --- a/COMPLETED_TASKS.md +++ b/COMPLETED_TASKS.md @@ -368,3 +368,30 @@ новая диагностика предназначена для следующего воспроизведения. Изменения только evening_detective_screen, сервер и схема БД не менялись. Установочные архивы не пересобирались; commit/push не выполнялись. + +## SCREEN-019 — Клиент API из proto + +- Статус: `DONE`. +- Дата: 2026-10-18. +- Результат: добавлена npm run proto:gen с protoc 36.0 и тем же плагином, + что во frontend (protoc-gen-typescript-http v0.10.0). Источник — канонический + ../evening_detective_server/api/main.proto; генерация пишет только в screen. + Сгенерированный клиент хранится в electron/api/generated; сборка не требует + генератора или соседних репозиториев. Генерируемые файлы исключены из линтера. +- Все API-вызовы Online и загрузчика описания переведены на сгенерированные методы: + вход, refresh, пользователь, игры, план, состояние/привязка/звук экрана, + музыка и результаты. Удалён ручной сборщик API-путей/JSON-тел. Локальные режимы + преобразуются в строковые enum proto; сохранены проверки ответов и точность + строковых int64 через BigInt, авторизация, таймауты, отмена и диагностика. + Медиа, кеш и воспроизведение остаются локальными; runtime-зависимости не добавлены. +- Проверки: повторная генерация побайтово идентична; 109 тестов, type-check, + lint, lint:css, сборка, Electron GUI smoke, 33 локальные Markdown-ссылки + и git diff --check — PASS. Добавлена проверка HTTP-методов/маршрутов, JSON enum, + отсутствующих полей и int64 выше Number.MAX_SAFE_INTEGER. GUI проверяет вход, + подключение, план, команды, музыку, дозагрузку, переподключение и показ. +- Локальное API: сгенерированные Login/GetMe успешны; GetGames возвращает прежний + HTTP 500 из-за устаревшей схемы локальной БД. Production-вход и физический + проектор не проверялись. Декларации int64 генератора требуют сохранения проверки + границы JSON; напрямую трактовать их как number нельзя. Ограничение описано в README. +- Границы: изменения только evening_detective_screen. Сервер, frontend и proto + не менялись, установочные архивы не пересобирались; commit/push не выполнялись. diff --git a/README.md b/README.md index c2763d5..09c02cc 100644 --- a/README.md +++ b/README.md @@ -303,3 +303,28 @@ GUI smoke на стенде без внешнего дисплея модели native fullscreen проверяется при наличии реального второго экрана. Реальное предотвращение сна, маршрутизацию звука ОС и физическое переподключение оборудования проверять по [инструкции SCREEN-007](docs/SCREEN-007_ACCEPTANCE.md) на всей матрице. + +## Генерация клиента API + +Приложение использует HTTP-клиент из канонического +`../evening_detective_server/api/main.proto`; сервер и frontend при генерации не меняются. +Сгенерированные методы и типы находятся в `electron/api/generated` и хранятся в Git. +Обычная сборка использует готовые файлы и не требует соседнего репозитория или генератора. + +Для обновления нужны `protoc 36.0`, Go и тот же плагин, что во frontend: + +```sh +go install go.einride.tech/protoc-gen-typescript-http@v0.10.0 +npm run proto:gen +``` + +Добавьте каталог Go `bin` в PATH. Команда проверяет версии и пишет только в screen; +повторная генерация при неизменном proto должна давать тот же результат. +Генерируемые файлы не редактировать и не форматировать вручную. + +Транспорт остаётся в основном процессе Electron: авторизация, обновление токена, +отмена, таймауты и безопасная диагностика общие для вызовов клиента. Медиа скачиваются +отдельно и воспроизводятся с локального диска. Генератор задаёт методы и TypeScript-типы, +но не проверяет и не преобразует JSON. В частности, его декларация `number` для `int64` +не меняет строковый формат gateway: входящие версии проходят проверку и сравниваются +через `BigInt`; обращения к этим полям напрямую как к числам не допускаются. diff --git a/electron/api-client.test.ts b/electron/api-client.test.ts new file mode 100644 index 0000000..05f7835 --- /dev/null +++ b/electron/api-client.test.ts @@ -0,0 +1,80 @@ +import assert from 'node:assert/strict' +import { test } from 'node:test' +import { createEveningDetectiveServerClient } from './api/generated/crabs/evening_detective_server/index.js' +import { apiPlan, apiScreenMode, apiScreenCommand } from './api-values.js' +import { parsePlan } from './screen-plan.js' +import { parseRemote } from './remote-state.js' + +test('клиент proto задаёт маршруты, методы и JSON enum существующего gateway', async () => { + const calls: { path: string; method: string; body: string | null }[] = [] + const client = createEveningDetectiveServerClient(async (request) => { + calls.push(request) + return {} + }) + await client.Login({ email: 'dev@example.test', password: 'SECRET' }) + await client.Refresh({ refreshToken: 'SECRET' }) + await client.GetMe({}) + await client.GetGames({}) + await client.GetGame({ ID: 7 }) + await client.GetScreenPlan({ gameID: 7 }) + await client.GetGameMusic({ gameID: 7 }) + const plan = parsePlan({ + steps: [ + { mode: 8, text: 'Улика' }, + { mode: 7, timerDurationSeconds: 60 }, + ], + }) + await client.SaveScreenPlan({ gameID: 7, ...apiPlan(plan) }) + await client.SetScreenMode({ gameID: 7, mode: apiScreenMode(2) }) + await client.SendScreenCommand({ + gameID: 7, + command: apiScreenCommand(6), + positionMs: 1500, + volume: 50, + }) + assert.deepEqual( + calls.map((c) => [c.method, c.path]), + [ + ['POST', 'api/auth/login'], + ['POST', 'api/auth/refresh'], + ['GET', 'api/users/me'], + ['GET', 'api/games'], + ['GET', 'api/games/7'], + ['GET', 'api/games/7/screen-plan'], + ['GET', 'api/games/7/music'], + ['PUT', 'api/games/7/screen-plan'], + ['POST', 'api/games/7/screens/mode'], + ['POST', 'api/games/7/screens/command'], + ], + ) + assert.deepEqual(parsePlan(JSON.parse(calls[7]!.body!)), plan) + assert.equal(JSON.parse(calls[7]!.body!).steps[0].mode, 'SCREEN_MODE_TEXT') + assert.equal(JSON.parse(calls[8]!.body!).mode, 'SCREEN_MODE_INTRO') + assert.equal(JSON.parse(calls[9]!.body!).command, 'SCREEN_COMMAND_SEEK') + assert.throws(() => apiScreenMode(99), /режим/) + assert.throws(() => apiScreenCommand(99), /команда/) +}) + +test('ответ proto сохраняет строковый int64, enum и отсутствующие поля для проверки границы', async () => { + const client = createEveningDetectiveServerClient(async () => + JSON.parse( + '{"state":{"stateVersion":"9007199254740993","mode":"SCREEN_MODE_TEXT","text":"Текст","positionMs":"1500"}}', + ), + ) + const next = parseRemote( + (await client.GetScreenState({ screenID: '12', accessToken: 'SECRET' })).state, + ) + assert.equal(next.version, 9007199254740993n) + assert.equal(next.mode, 8) + assert.equal(next.position, 1.5) + assert.equal(next.music, null) + const invalid = createEveningDetectiveServerClient(async () => ({ + state: { stateVersion: 9007199254740992 }, + })) + assert.throws(() => parseRemote({ stateVersion: 9007199254740992 }), /версия/) + await assert.rejects( + async () => + parseRemote((await invalid.GetScreenState({ screenID: '12', accessToken: 'SECRET' })).state), + /версия/, + ) +}) diff --git a/electron/api-values.ts b/electron/api-values.ts new file mode 100644 index 0000000..53df23d --- /dev/null +++ b/electron/api-values.ts @@ -0,0 +1,44 @@ +import type { + ScreenCommand, + ScreenMode, + SaveScreenPlanReq, +} from './api/generated/crabs/evening_detective_server/index.js' +import type { ScreenPlan } from '../src/types/screen.js' +const modes = [ + 'SCREEN_MODE_UNSPECIFIED', + 'SCREEN_MODE_COVER', + 'SCREEN_MODE_INTRO', + 'SCREEN_MODE_INTRO_DONE', + 'SCREEN_MODE_CONCLUSION', + 'SCREEN_MODE_CONCLUSION_DONE', + 'SCREEN_MODE_RESULTS', + 'SCREEN_MODE_TIMER', + 'SCREEN_MODE_TEXT', +] as const satisfies readonly ScreenMode[] +const commands = [ + 'SCREEN_COMMAND_UNSPECIFIED', + 'SCREEN_COMMAND_PLAY', + 'SCREEN_COMMAND_PAUSE', + 'SCREEN_COMMAND_RESUME', + 'SCREEN_COMMAND_STOP', + 'SCREEN_COMMAND_REPLAY', + 'SCREEN_COMMAND_SEEK', + 'SCREEN_COMMAND_VOLUME', + 'SCREEN_COMMAND_TIMER_START', + 'SCREEN_COMMAND_TIMER_PAUSE', + 'SCREEN_COMMAND_TIMER_RESUME', + 'SCREEN_COMMAND_TIMER_RESET', +] as const satisfies readonly ScreenCommand[] +export function apiScreenMode(mode: number): ScreenMode { + const value = modes[mode] + if (!value) throw new Error('Некорректный режим экрана') + return value +} +export function apiScreenCommand(command: number): ScreenCommand { + const value = commands[command] + if (!value) throw new Error('Некорректная команда экрана') + return value +} +export function apiPlan(plan: ScreenPlan): Pick { + return { ...plan, steps: plan.steps.map((step) => ({ ...step, mode: apiScreenMode(step.mode) })) } +} diff --git a/electron/api/generated/crabs/evening_detective_server/index.ts b/electron/api/generated/crabs/evening_detective_server/index.ts new file mode 100644 index 0000000..e91897c --- /dev/null +++ b/electron/api/generated/crabs/evening_detective_server/index.ts @@ -0,0 +1,3742 @@ +// Code generated by protoc-gen-typescript-http. DO NOT EDIT. +/* eslint-disable camelcase */ +// @ts-nocheck + +export type ScreenMode = + | "SCREEN_MODE_UNSPECIFIED" + | "SCREEN_MODE_COVER" + | "SCREEN_MODE_INTRO" + | "SCREEN_MODE_INTRO_DONE" + | "SCREEN_MODE_CONCLUSION" + | "SCREEN_MODE_CONCLUSION_DONE" + | "SCREEN_MODE_RESULTS" + | "SCREEN_MODE_TIMER" + | "SCREEN_MODE_TEXT"; +export type ScreenTimerState = + | "SCREEN_TIMER_STATE_UNSPECIFIED" + | "SCREEN_TIMER_STATE_STOPPED" + | "SCREEN_TIMER_STATE_RUNNING" + | "SCREEN_TIMER_STATE_PAUSED" + | "SCREEN_TIMER_STATE_FINISHED"; +export type ScreenCommand = + | "SCREEN_COMMAND_UNSPECIFIED" + | "SCREEN_COMMAND_PLAY" + | "SCREEN_COMMAND_PAUSE" + | "SCREEN_COMMAND_RESUME" + | "SCREEN_COMMAND_STOP" + | "SCREEN_COMMAND_REPLAY" + | "SCREEN_COMMAND_SEEK" + | "SCREEN_COMMAND_VOLUME" + | "SCREEN_COMMAND_TIMER_START" + | "SCREEN_COMMAND_TIMER_PAUSE" + | "SCREEN_COMMAND_TIMER_RESUME" + | "SCREEN_COMMAND_TIMER_RESET"; +export type MusicPlaybackState = + | "MUSIC_PLAYBACK_UNSPECIFIED" + | "MUSIC_PLAYBACK_STOPPED" + | "MUSIC_PLAYBACK_PLAYING" + | "MUSIC_PLAYBACK_PAUSED"; +export type PingReq = { +}; + +export type PingRsp = { +}; + +export type EchoReq = { + text: string | undefined; +}; + +export type EchoRsp = { + text: string | undefined; +}; + +export type SignupReq = { + username: string | undefined; + email: string | undefined; + acceptTerms: boolean | undefined; + acceptPrivacy: boolean | undefined; +}; + +export type SignupRsp = { +}; + +export type RefreshPasswordReq = { + email: string | undefined; +}; + +export type RefreshPasswordRsp = { +}; + +export type LoginReq = { + email: string | undefined; + password: string | undefined; +}; + +export type LoginRsp = { + accessToken: string | undefined; + refreshToken: string | undefined; +}; + +export type RefreshReq = { + refreshToken: string | undefined; +}; + +export type RefreshRsp = { + accessToken: string | undefined; + refreshToken: string | undefined; +}; + +// Пароль подтверждает, что удаляет аккаунт его владелец, а не обладатель +// украденного access-токена (ст. 21 152-ФЗ). Передаётся HTTP-заголовком +// X-Password (пробрасывается матчером в cmd/evening_detective_server/main.go), +// чтобы креденшел не попадал в URL/query-параметры и логи. +export type DeleteAccountReq = { +}; + +export type DeleteAccountRsp = { +}; + +// Документы раздаются по постоянным URL, на которые ссылается сервис при +// фиксации акцептов соглашений (user_agreements) и форма регистрации. +export type GetTermsReq = { +}; + +export type GetTermsRsp = { + text: string | undefined; +}; + +export type GetPrivacyReq = { +}; + +export type GetPrivacyRsp = { + text: string | undefined; +}; + +export type GetConsentReq = { +}; + +export type GetConsentRsp = { + text: string | undefined; +}; + +export type GetUsersReq = { +}; + +export type GetUsersRsp = { + users: User[] | undefined; +}; + +export type User = { + ID: number | undefined; + username: string | undefined; + email: string | undefined; + roles: string[] | undefined; + isActive: boolean | undefined; + createdAt: wellKnownTimestamp | undefined; +}; + +// Encoded using RFC 3339, where generated output will always be Z-normalized +// and uses 0, 3, 6 or 9 fractional digits. +// Offsets other than "Z" are also accepted. +type wellKnownTimestamp = string; + +export type GetUserByIDReq = { + ID: number | undefined; +}; + +export type GetUserByIDRsp = { + user: User | undefined; +}; + +export type GetMeReq = { +}; + +export type GetMeRsp = { + user: User | undefined; +}; + +export type ChangeUserPasswordReq = { + ID: number | undefined; + newPassword: string | undefined; +}; + +export type ChangeUserPasswordRsp = { +}; + +export type AddUserRoleReq = { + ID: number | undefined; + role: string | undefined; +}; + +export type AddUserRoleRsp = { +}; + +export type DeleteUserRoleReq = { + ID: number | undefined; + role: string | undefined; +}; + +export type DeleteUserRoleRsp = { +}; + +export type GetPermissionsReq = { +}; + +export type GetPermissionsRsp = { + permissions: string[] | undefined; + // Actual server environment: prod or venue. + environment: string | undefined; +}; + +export type UploadFileReq = { + filename: string | undefined; + data: string | undefined; +}; + +export type UploadFileRsp = { + filename: string | undefined; + // Тип загруженного файла: image | pdf | audio | video (определяется сервером по + // содержимому). Удобен редактору для предзаполнения file_type улики. + file_type: string | undefined; +}; + +export type DownloadFileReq = { + filename: string | undefined; +}; + +export type PlaylistTrack = { + ID: number | undefined; + name: string | undefined; + filename: string | undefined; + position: number | undefined; +}; + +export type Playlist = { + ID: number | undefined; + name: string | undefined; + tracks: PlaylistTrack[] | undefined; +}; + +export type GetPlaylistsReq = { +}; + +export type GetPlaylistsRsp = { + playlists: Playlist[] | undefined; +}; + +export type CreatePlaylistReq = { + name: string | undefined; +}; + +export type CreatePlaylistRsp = { + playlist: Playlist | undefined; +}; + +export type UpdatePlaylistReq = { + ID: number | undefined; + name: string | undefined; +}; + +export type UpdatePlaylistRsp = { + playlist: Playlist | undefined; +}; + +export type DeletePlaylistReq = { + ID: number | undefined; +}; + +export type DeletePlaylistRsp = { +}; + +export type UploadPlaylistTrackReq = { + playlistID: number | undefined; + name: string | undefined; + filename: string | undefined; + data: string | undefined; +}; + +export type UploadPlaylistTrackRsp = { + playlist: Playlist | undefined; +}; + +export type DeletePlaylistTrackReq = { + playlistID: number | undefined; + trackID: number | undefined; +}; + +export type DeletePlaylistTrackRsp = { +}; + +export type SetScenarioPlaylistReq = { + ID: number | undefined; + playlistID: number | undefined; +}; + +export type SetScenarioPlaylistRsp = { +}; + +export type AddScenarioReq = { + name: string | undefined; +}; + +export type AddScenarioRsp = { + ID: number | undefined; +}; + +export type GetMyScenariosReq = { +}; + +export type GetMyScenariosRsp = { + scenarios: Scenario[] | undefined; +}; + +export type Scenario = { + ID: number | undefined; + name: string | undefined; + description?: string; + image?: string; + story: Story | undefined; + status: string | undefined; + author: User | undefined; + updatedAt: wellKnownTimestamp | undefined; + createdAt: wellKnownTimestamp | undefined; + publishedAt: wellKnownTimestamp | undefined; + averageRating: number | undefined; + reviewsCount: number | undefined; + difficulty: string | undefined; + music_playlist_id?: number; +}; + +export type Story = { + places: Place[] | undefined; + introduction: Introduction | undefined; + questions: Question[] | undefined; + background: string | undefined; + conclusion: Conclusion | undefined; +}; + +export type Place = { + code: string | undefined; + name: string | undefined; + text: string | undefined; + image: string | undefined; + hidden: boolean | undefined; + applications: Application[] | undefined; + doors: Door[] | undefined; + keys: Key[] | undefined; +}; + +export type Application = { + name: string | undefined; + image: string | undefined; + // Тип файла улики: image | pdf | audio. Значение выводится сервером из + // расширения файла (см. file_storage.FileType); пустое значение возможно + // только для legacy-строк без расширения. + file_type: string | undefined; +}; + +export type Door = { + code: string | undefined; + name: string | undefined; + keys: Key[] | undefined; +}; + +export type Key = { + name: string | undefined; +}; + +export type Introduction = { + text: string | undefined; + audio: string | undefined; +}; + +// Вопрос дела - вопрос, на который команда должна ответить в конце игры. +// Правильный ответ (answer) виден только автору сценария и организаторам +// (при оценке ответов команд), команде не передаётся. +export type Question = { + // Код вопроса - используется при отправке ответов командой + code: string | undefined; + // Текст вопроса + text: string | undefined; + // Правильный ответ (справка для организатора при оценке) + answer: string | undefined; +}; + +export type Conclusion = { + text: string | undefined; + video: string | undefined; +}; + +export type GetScenariosCatalogReq = { +}; + +export type GetScenariosCatalogRsp = { + scenarios: Scenario[] | undefined; +}; + +export type GetScenarioReq = { + ID: number | undefined; +}; + +export type GetScenarioRsp = { + scenario: Scenario | undefined; +}; + +// Ответ команды на вопрос дела +export type Answer = { + // Код вопроса + questionCode: string | undefined; + // Текст ответа команды + answer: string | undefined; +}; + +// Ответ команды с баллом (для панели организатора) +export type AnswerScore = { + questionCode: string | undefined; + answer: string | undefined; + // Балл, выставленный организатором; отсутствует, если ещё не оценено + score?: number; +}; + +// Ответы одной команды на вопросы дела +export type TeamAnswers = { + team: Team | undefined; + answers: AnswerScore[] | undefined; +}; + +export type Team = { + ID: string | undefined; + name: string | undefined; + status: string | undefined; + password: string | undefined; + actionsCount: number | undefined; + applications: Application[] | undefined; + captainEmail: string | undefined; + participantsCount: number | undefined; + // Статус участия: pending, confirmed или cancelled. Поле панели организатора. + participationStatus: string | undefined; + // Источник решения: captain, automatic либо пустая строка до ответа. + participationSource: string | undefined; + // Последняя ошибка отправки; пустая строка, если ошибки нет. + participationEmailError: string | undefined; + // Письмо принято SMTP-сервером; не означает прочтение или доставку во входящие. + participationEmailSent: boolean | undefined; +}; + +export type UpdateScenarioReq = { + ID: number | undefined; + name: string | undefined; + description: string | undefined; + image: string | undefined; +}; + +export type UpdateScenarioRsp = { +}; + +export type PublicScenarioReq = { + ID: number | undefined; +}; + +export type PublicScenarioRsp = { +}; + +export type DraftScenarioReq = { + ID: number | undefined; +}; + +export type DraftScenarioRsp = { +}; + +export type DeleteScenarioReq = { + ID: number | undefined; +}; + +export type DeleteScenarioRsp = { +}; + +export type AddScenarioPlaceReq = { + ID: number | undefined; + place: Place | undefined; +}; + +export type AddScenarioPlaceRsp = { +}; + +export type UpdateScenarioPlaceReq = { + ID: number | undefined; + code: string | undefined; + place: Place | undefined; +}; + +export type UpdateScenarioPlaceRsp = { +}; + +export type DeleteScenarioPlaceReq = { + ID: number | undefined; + code: string | undefined; +}; + +export type DeleteScenarioPlaceRsp = { +}; + +export type UpdateScenarioConclusionReq = { + ID: number | undefined; + conclusion: Conclusion | undefined; +}; + +export type UpdateScenarioConclusionRsp = { +}; + +export type UpdateScenarioIntroReq = { + ID: number | undefined; + introduction: Introduction | undefined; +}; + +export type UpdateScenarioIntroRsp = { +}; + +export type AddScenarioQuestionReq = { + ID: number | undefined; + question: Question | undefined; +}; + +export type AddScenarioQuestionRsp = { +}; + +export type UpdateScenarioQuestionReq = { + ID: number | undefined; + code: string | undefined; + question: Question | undefined; +}; + +export type UpdateScenarioQuestionRsp = { +}; + +export type DeleteScenarioQuestionReq = { + ID: number | undefined; + code: string | undefined; +}; + +export type DeleteScenarioQuestionRsp = { +}; + +export type DownloadScenarioArchiveReq = { + ID: number | undefined; +}; + +export type UploadScenarioArchiveRsp = { + ID: number | undefined; +}; + +export type AddGameReq = { + name: string | undefined; + description: string | undefined; + startAt: wellKnownTimestamp | undefined; + scenarioID: number | undefined; + format: string | undefined; + location: string | undefined; +}; + +export type AddGameRsp = { + ID: number | undefined; +}; + +export type GetGamesReq = { +}; + +export type GetGamesRsp = { + games: Game[] | undefined; +}; + +export type Game = { + ID: number | undefined; + name: string | undefined; + description: string | undefined; + startAt: wellKnownTimestamp | undefined; + status: string | undefined; + scenario: Scenario | undefined; + teams: Team[] | undefined; + startedAt?: wellKnownTimestamp; + endedAt?: wellKnownTimestamp; + format: string | undefined; + location: string | undefined; + export: GameExport | undefined; +}; + +// Download ready ZIP through the authenticated streaming HTTP route: +// GET /api/games/{ID}/export/download (not an HttpBody RPC). +export type GameExport = { + ID: string | undefined; + state: string | undefined; + packageID: string | undefined; + size: number | undefined; + sha256: string | undefined; + failure: string | undefined; +}; + +export type GameImportRsp = { + gameID: number | undefined; + packageID: string | undefined; + repeated: boolean | undefined; +}; + +export type StartGameExportReq = { + ID: number | undefined; +}; + +export type GetGameExportReq = { + ID: number | undefined; +}; + +export type ResetGameLocalModeReq = { + ID: number | undefined; + exportID: string | undefined; +}; + +export type ResetGameLocalModeRsp = { +}; + +export type GameExportRsp = { + export: GameExport | undefined; +}; + +export type ScreenState = { + ID: string | undefined; + status: string | undefined; + gameID?: number; + mode: ScreenMode | undefined; + soundEnabled: boolean | undefined; + error: string | undefined; + stateVersion: number | undefined; + positionMs: number | undefined; + gameName: string | undefined; + coverImage: string | undefined; + introAudio: string | undefined; + conclusionVideo: string | undefined; + command: ScreenCommand | undefined; + volume: number | undefined; + backgroundImage: string | undefined; + timerLabel: string | undefined; + timerDurationSeconds: number | undefined; + timerRemainingSeconds: number | undefined; + timerState: ScreenTimerState | undefined; + text: string | undefined; + music: MusicState | undefined; +}; + +export type MusicState = { + playlist: Playlist | undefined; + trackIndex: number | undefined; + positionMs: number | undefined; + volume: number | undefined; + loop: boolean | undefined; + playback: MusicPlaybackState | undefined; + stateVersion: number | undefined; +}; + +export type CreateScreenSessionReq = { +}; + +export type CreateScreenSessionRsp = { + screenID: string | undefined; + accessToken: string | undefined; +}; + +export type GetScreenStateReq = { + screenID: string | undefined; + accessToken: string | undefined; +}; + +export type GetScreenStateRsp = { + state: ScreenState | undefined; +}; + +export type GetGameScreensReq = { + gameID: number | undefined; +}; + +export type GetGameScreensRsp = { + screens: ScreenState[] | undefined; +}; + +export type GetGameMusicReq = { + gameID: number | undefined; +}; + +export type GetGameMusicRsp = { + playlists: Playlist[] | undefined; + state: MusicState | undefined; + scenarioPlaylistId: number | undefined; + gamePlaylistId: number | undefined; +}; + +export type SetGameMusicReq = { + gameID: number | undefined; + playlistID: number | undefined; + trackIndex: number | undefined; + positionMs: number | undefined; + volume: number | undefined; + loop: boolean | undefined; + playback: MusicPlaybackState | undefined; +}; + +export type SetGameMusicRsp = { + state: MusicState | undefined; +}; + +export type AdvanceScreenMusicReq = { + screenID: string | undefined; + accessToken: string | undefined; +}; + +export type AdvanceScreenMusicRsp = { + state: MusicState | undefined; +}; + +export type BindScreenReq = { + screenID: string | undefined; + accessToken: string | undefined; + gameID: number | undefined; +}; + +export type BindScreenRsp = { + state: ScreenState | undefined; +}; + +export type UnbindScreenReq = { + screenID: string | undefined; +}; + +export type UnbindScreenRsp = { +}; + +export type SetScreenSoundReq = { + screenID: string | undefined; + soundEnabled: boolean | undefined; +}; + +export type SetScreenSoundRsp = { +}; + +export type MapItem = { + kind: string | undefined; + itemID: string | undefined; + label: string | undefined; + x: number | undefined; + y: number | undefined; + hidden: boolean | undefined; + status: string | undefined; + soundEnabled: boolean | undefined; +}; + +export type GetGameMapReq = { + gameID: number | undefined; +}; + +export type GetGameMapRsp = { + items: MapItem[] | undefined; +}; + +export type UpdateGameMapItemReq = { + gameID: number | undefined; + kind: string | undefined; + itemID: string | undefined; + x: number | undefined; + y: number | undefined; + hidden: boolean | undefined; +}; + +export type UpdateGameMapItemRsp = { +}; + +export type SetScreenModeReq = { + gameID: number | undefined; + mode: ScreenMode | undefined; +}; + +export type SetScreenModeRsp = { +}; + +export type SetScreenTimerReq = { + gameID: number | undefined; + durationSeconds: number | undefined; + label: string | undefined; +}; + +export type SetScreenTimerRsp = { +}; + +export type SendScreenCommandReq = { + gameID: number | undefined; + command: ScreenCommand | undefined; + positionMs: number | undefined; + volume: number | undefined; +}; + +export type SendScreenCommandRsp = { +}; + +export type ScreenPlanStep = { + mode: ScreenMode | undefined; + text: string | undefined; + timerLabel: string | undefined; + timerDurationSeconds: number | undefined; +}; + +export type ScreenPlan = { + steps: ScreenPlanStep[] | undefined; + currentStep: number | undefined; +}; + +export type GetScreenPlanReq = { + gameID: number | undefined; +}; + +export type GetScreenPlanRsp = { + plan: ScreenPlan | undefined; +}; + +export type SaveScreenPlanReq = { + gameID: number | undefined; + steps: ScreenPlanStep[] | undefined; + currentStep: number | undefined; +}; + +export type SaveScreenPlanRsp = { + plan: ScreenPlan | undefined; +}; + +export type SetScreenPlanStepReq = { + gameID: number | undefined; + currentStep: number | undefined; +}; + +export type SetScreenPlanStepRsp = { + plan: ScreenPlan | undefined; +}; + +export type GetScreenResultsReq = { + screenID: string | undefined; + accessToken: string | undefined; +}; + +export type GetScreenResultsRsp = { + questions: ResultsQuestion[] | undefined; + teams: TeamResult[] | undefined; +}; + +// Вопрос в шапке итоговой таблицы: правильный ответ (поле answer сообщения +// Question) командам/таблице не передаётся — он виден только в панели оценки. +export type ResultsQuestion = { + code: string | undefined; + text: string | undefined; +}; + +// Строка итоговой таблицы: команда, баллы по вопросам, сумма и место. +export type TeamResult = { + team: Team | undefined; + answers: AnswerScore[] | undefined; + // Сумма баллов по всем вопросам сценария (неоценённые считаются как 0) + totalScore: number | undefined; + // Место в итоговой таблице: команды с одинаковой суммой баллов и + // одинаковым числом ходов занимают одно место (следующее пропускается) + place: number | undefined; + // Количество ходов команды (дублирует team.actions_count для удобства + // таблицы; участвует в тай-брейке при равенстве суммы баллов) + actionsCount: number | undefined; +}; + +export type GetParticipationReq = { + // Персональный токен из письма: 64 шестнадцатеричных символа. Не является JWT. + token: string | undefined; +}; + +export type AnswerParticipationReq = { + // Персональный токен из письма: 64 шестнадцатеричных символа. Не является JWT. + token: string | undefined; + // Окончательное решение: confirmed — участвуем, cancelled — отменяем запись. + answer: string | undefined; +}; + +export type ParticipationRsp = { + // Название команды. Данные доступны только по действительному токену. + teamName: string | undefined; + // Название игры. + gameName: string | undefined; + // Плановое начало игры, UTC (RFC 3339 в JSON). + startAt: wellKnownTimestamp | undefined; + // Заявленное число участников команды. + participantsCount: number | undefined; + // pending — ожидает ответа, confirmed — подтверждено, cancelled — отменено. + status: string | undefined; + // Новые ответы закрыты: наступило плановое начало или игра вышла из draft. + // Изменение уже принятого решения запрещено независимо от этого поля. + closed: boolean | undefined; +}; + +export type GetGameReq = { + ID: number | undefined; +}; + +export type GetGameRsp = { + game: Game | undefined; +}; + +export type GetGameAnswersReq = { + gameID: number | undefined; +}; + +export type GetGameAnswersRsp = { + questions: Question[] | undefined; + teams: TeamAnswers[] | undefined; +}; + +export type GetGameResultsReq = { + gameID: number | undefined; +}; + +export type GetGameResultsRsp = { + questions: ResultsQuestion[] | undefined; + teams: TeamResult[] | undefined; +}; + +export type GameReview = { + ID: number | undefined; + gameID: number | undefined; + teamID: string | undefined; + teamName: string | undefined; + rating: number | undefined; + text: string | undefined; + status: string | undefined; + createdAt: wellKnownTimestamp | undefined; + gameName: string | undefined; + difficulty: number | undefined; +}; + +export type CreateGameReviewReq = { + gameID: number | undefined; + teamID: string | undefined; + password: string | undefined; + rating: number | undefined; + text: string | undefined; + difficulty: number | undefined; +}; + +export type CreateGameReviewRsp = { + review: GameReview | undefined; +}; + +export type GetGameReviewsReq = { + gameID: number | undefined; +}; + +export type GetScenarioReviewsReq = { + scenarioID: number | undefined; +}; + +export type GetReviewsForModerationReq = { +}; + +export type GetGameReviewsRsp = { + averageRating: number | undefined; + reviewsCount: number | undefined; + reviews: GameReview[] | undefined; +}; + +export type ModerateGameReviewReq = { + ID: number | undefined; + status: string | undefined; +}; + +export type ModerateGameReviewRsp = { + review: GameReview | undefined; +}; + +export type UpdateGameReq = { + ID: number | undefined; + name: string | undefined; + description: string | undefined; + startAt: wellKnownTimestamp | undefined; + scenarioID: number | undefined; + format: string | undefined; + location: string | undefined; +}; + +export type UpdateGameRsp = { +}; + +export type StartGameReq = { + ID: number | undefined; +}; + +export type StartGameRsp = { +}; + +export type PauseGameReq = { + ID: number | undefined; +}; + +export type PauseGameRsp = { +}; + +export type PlayGameReq = { + ID: number | undefined; +}; + +export type PlayGameRsp = { +}; + +export type FinishGameReq = { + ID: number | undefined; +}; + +export type FinishGameRsp = { +}; + +export type ResetGameReq = { + ID: number | undefined; +}; + +export type ResetGameRsp = { +}; + +export type DeleteGameReq = { + ID: number | undefined; +}; + +export type DeleteGameRsp = { +}; + +export type AddTeamReq = { + gameID: number | undefined; + name: string | undefined; + captainEmail: string | undefined; + participantsCount: number | undefined; +}; + +export type AddTeamRsp = { + ID: string | undefined; + password: string | undefined; +}; + +export type UpdateTeamReq = { + ID: string | undefined; + name: string | undefined; +}; + +export type UpdateTeamRsp = { +}; + +export type DeleteTeamReq = { + ID: string | undefined; +}; + +export type DeleteTeamRsp = { +}; + +export type GiveTeamApplicationsReq = { + ID: string | undefined; + name: string | undefined; +}; + +export type GiveTeamApplicationsRsp = { +}; + +export type GetTeamStoryReq = { + ID: string | undefined; + password: string | undefined; +}; + +export type GetTeamStoryRsp = { + story: Story | undefined; + game: Game | undefined; + actions: TeamAction[] | undefined; +}; + +export type TeamAction = { + code: string | undefined; + mcp: boolean | undefined; +}; + +export type AddTeamActionReq = { + ID: string | undefined; + password: string | undefined; + code: string | undefined; +}; + +export type AddTeamActionRsp = { +}; + +export type DeleteLastTeamActionReq = { + ID: string | undefined; +}; + +export type DeleteLastTeamActionRsp = { +}; + +export type GetTeamAnswersReq = { + ID: string | undefined; + password: string | undefined; +}; + +export type GetTeamAnswersRsp = { + answers: Answer[] | undefined; +}; + +export type SubmitTeamAnswersReq = { + ID: string | undefined; + password: string | undefined; + answers: Answer[] | undefined; +}; + +export type SubmitTeamAnswersRsp = { +}; + +export type SetTeamAnswerScoreReq = { + ID: string | undefined; + questionCode: string | undefined; + // Балл; отсутствует — сбросить выставленный балл + score?: number; +}; + +export type SetTeamAnswerScoreRsp = { +}; + +export type UpdateScenarioBackgroundReq = { + ID: number | undefined; + background: string | undefined; +}; + +export type UpdateScenarioBackgroundRsp = { +}; + +export type IssueGameTicketsReq = { + gameID: number | undefined; + count: number | undefined; +}; + +export type IssueGameTicketsRsp = { + tickets: Ticket[] | undefined; +}; + +export type Ticket = { + ID: number | undefined; + URL: string | undefined; +}; + +export type GetTicketReq = { + token: string | undefined; +}; + +export type GetTicketRsp = { + ticket: TicketPage | undefined; +}; + +export type TicketPage = { + ID: number | undefined; + gameName: string | undefined; + startAt: wellKnownTimestamp | undefined; + location: string | undefined; + status: string | undefined; + gameStatus: string | undefined; + usedAt?: wellKnownTimestamp; + usedBy: string | undefined; +}; + +export type AcceptTicketReq = { + token: string | undefined; +}; + +export type AcceptTicketRsp = { + ticket: TicketPage | undefined; +}; + +// Developer tasks use new, in_progress, review and done statuses. +export type DeveloperTask = { + ID: number | undefined; + title: string | undefined; + description: string | undefined; + status: string | undefined; + // Omitted assignee means the task is unassigned. + assigneeID?: number; + assigneeName: string | undefined; + authorID?: number; + // Calendar deadline in YYYY-MM-DD format; empty means no deadline. + dueDate: string | undefined; + createdAt: wellKnownTimestamp | undefined; + updatedAt: wellKnownTimestamp | undefined; + // Required for updates and deletion to detect concurrent edits. + version: number | undefined; +}; + +export type TaskAssignee = { + ID: number | undefined; + name: string | undefined; +}; + +export type GetTasksReq = { +}; + +export type GetTasksRsp = { + tasks: DeveloperTask[] | undefined; +}; + +export type GetTaskAssigneesReq = { +}; + +export type GetTaskAssigneesRsp = { + assignees: TaskAssignee[] | undefined; +}; + +export type CreateTaskReq = { + title: string | undefined; + description: string | undefined; + status: string | undefined; + assigneeID?: number; + dueDate: string | undefined; +}; + +export type UpdateTaskReq = { + ID: number | undefined; + title: string | undefined; + description: string | undefined; + status: string | undefined; + assigneeID?: number; + dueDate: string | undefined; + version: number | undefined; +}; + +export type SetTaskStatusReq = { + ID: number | undefined; + status: string | undefined; + version: number | undefined; +}; + +export type DeleteTaskReq = { + ID: number | undefined; + version: number | undefined; +}; + +export type TaskRsp = { + task: DeveloperTask | undefined; +}; + +export type DeleteTaskRsp = { +}; + +export type GameResultsPackageReq = { + ID: number | undefined; +}; + +export type ApplyGameResultsPackageReq = { + ID: number | undefined; + package: googleapi_HttpBody | undefined; +}; + +// Message that represents an arbitrary HTTP body. It should only be used for +// payload formats that can't be represented as JSON, such as raw binary or +// an HTML page. +// This message can be used both in streaming and non-streaming API methods in +// the request as well as the response. +// It can be used as a top-level request field, which is convenient if one +// wants to extract parameters from either the URL or HTTP template into the +// request fields and also want access to the raw HTTP body. +// Example: +// message GetResourceRequest { +// // A unique request id. +// string request_id = 1; +// // The raw HTTP body is bound to this field. +// google.api.HttpBody http_body = 2; +// } +// service ResourceService { +// rpc GetResource(GetResourceRequest) +// returns (google.api.HttpBody); +// rpc UpdateResource(google.api.HttpBody) +// returns (google.protobuf.Empty); +// } +// Example with streaming methods: +// service CaldavService { +// rpc GetCalendar(stream google.api.HttpBody) +// returns (stream google.api.HttpBody); +// rpc UpdateCalendar(stream google.api.HttpBody) +// returns (stream google.api.HttpBody); +// } +// Use of this type only changes how the request and response bodies are +// handled, all other features will continue to work unchanged. +export type googleapi_HttpBody = { + // The HTTP Content-Type header value specifying the content type of the body. + contentType: string | undefined; + // The HTTP request/response body as raw binary. + data: string | undefined; + // Application specific response metadata. Must be set in the first response + // for streaming APIs. + extensions: wellKnownAny[] | undefined; +}; + +// If the Any contains a value that has a special JSON mapping, +// it will be converted as follows: +// {"@type": xxx, "value": yyy}. +// Otherwise, the value will be converted into a JSON object, +// and the "@type" field will be inserted to indicate the actual data type. +interface wellKnownAny { + "@type": string; + [key: string]: unknown; +} + +export type GameResultsPackageRsp = { + gameID: number | undefined; + packageID: string | undefined; + appliedAt: string | undefined; +}; + +export type DeviceHeartbeatReq = { + deviceID: string | undefined; + connectionID: string | undefined; + organizerGameID?: number; + teamID?: string; + password: string | undefined; + screenID: string | undefined; + accessToken: string | undefined; +}; + +export type DeviceHeartbeatRsp = { + enabled?: boolean; + serverTime: wellKnownTimestamp | undefined; +}; + +export type GetDevicesReq = { + gameID?: number; +}; + +export type GetDevicesRsp = { + devices: VenueDevice[] | undefined; + serverTime: wellKnownTimestamp | undefined; + wifi: VenueWiFi | undefined; +}; + +export type VenueDevice = { + ID: string | undefined; + firstSeenAt: wellKnownTimestamp | undefined; + lastSeenAt: wellKnownTimestamp | undefined; + connected: boolean | undefined; + connections: DeviceConnection[] | undefined; +}; + +export type DeviceConnection = { + ID: string | undefined; + firstSeenAt: wellKnownTimestamp | undefined; + lastSeenAt: wellKnownTimestamp | undefined; + IP: string | undefined; + userAgent: string | undefined; + connected: boolean | undefined; + gameID?: number; + teamID?: string; + screenID?: number; + kind: string | undefined; +}; + +export type VenueWiFi = { + enabled: boolean | undefined; + collectedAt: wellKnownTimestamp | undefined; + stale: boolean | undefined; + hasError: boolean | undefined; + clients: WiFiClient[] | undefined; +}; + +export type WiFiClient = { + IP: string | undefined; + MAC: string | undefined; + band: string | undefined; + signal?: number; +}; + +export type TeamReceiptReq = { + gameID: number | undefined; + teamID: string | undefined; +}; + +export type PrintTeamReceiptRsp = { +}; + +export interface EveningDetectiveServer { + DeviceHeartbeat(request: DeviceHeartbeatReq): Promise; + GetDevices(request: GetDevicesReq): Promise; + GetTasks(request: GetTasksReq): Promise; + GetTaskAssignees(request: GetTaskAssigneesReq): Promise; + CreateTask(request: CreateTaskReq): Promise; + UpdateTask(request: UpdateTaskReq): Promise; + SetTaskStatus(request: SetTaskStatusReq): Promise; + DeleteTask(request: DeleteTaskReq): Promise; + GetParticipation(request: GetParticipationReq): Promise; + AnswerParticipation(request: AnswerParticipationReq): Promise; + Ping(request: PingReq): Promise; + Echo(request: EchoReq): Promise; + Signup(request: SignupReq): Promise; + RefreshPassword(request: RefreshPasswordReq): Promise; + Login(request: LoginReq): Promise; + Refresh(request: RefreshReq): Promise; + DeleteAccount(request: DeleteAccountReq): Promise; + GetTerms(request: GetTermsReq): Promise; + GetPrivacy(request: GetPrivacyReq): Promise; + GetConsent(request: GetConsentReq): Promise; + GetUsers(request: GetUsersReq): Promise; + GetUserByID(request: GetUserByIDReq): Promise; + GetMe(request: GetMeReq): Promise; + // Только администратор; сеансы пользователя сохраняются. + // Ошибки: 401, 403, 400, 404, 500. + ChangeUserPassword(request: ChangeUserPasswordReq): Promise; + AddUserRole(request: AddUserRoleReq): Promise; + DeleteUserRole(request: DeleteUserRoleReq): Promise; + GetPermissions(request: GetPermissionsReq): Promise; + UploadFile(request: UploadFileReq): Promise; + DownloadFile(request: DownloadFileReq): Promise; + GetPlaylists(request: GetPlaylistsReq): Promise; + CreatePlaylist(request: CreatePlaylistReq): Promise; + UpdatePlaylist(request: UpdatePlaylistReq): Promise; + DeletePlaylist(request: DeletePlaylistReq): Promise; + UploadPlaylistTrack(request: UploadPlaylistTrackReq): Promise; + DeletePlaylistTrack(request: DeletePlaylistTrackReq): Promise; + SetScenarioPlaylist(request: SetScenarioPlaylistReq): Promise; + AddScenario(request: AddScenarioReq): Promise; + GetMyScenarios(request: GetMyScenariosReq): Promise; + GetScenariosCatalog(request: GetScenariosCatalogReq): Promise; + GetFullScenario(request: GetScenarioReq): Promise; + GetScenario(request: GetScenarioReq): Promise; + UpdateScenario(request: UpdateScenarioReq): Promise; + PublicScenario(request: PublicScenarioReq): Promise; + DraftScenario(request: DraftScenarioReq): Promise; + DeleteScenario(request: DeleteScenarioReq): Promise; + AddScenarioPlace(request: AddScenarioPlaceReq): Promise; + UpdateScenarioPlace(request: UpdateScenarioPlaceReq): Promise; + DeleteScenarioPlace(request: DeleteScenarioPlaceReq): Promise; + AddScenarioQuestion(request: AddScenarioQuestionReq): Promise; + UpdateScenarioQuestion(request: UpdateScenarioQuestionReq): Promise; + DeleteScenarioQuestion(request: DeleteScenarioQuestionReq): Promise; + // Загрузить файл через UploadFile, затем передать его имя; пустое имя удаляет фон дела. + UpdateScenarioBackground(request: UpdateScenarioBackgroundReq): Promise; + UpdateScenarioIntro(request: UpdateScenarioIntroReq): Promise; + // Видео загружается через UploadFile (MP4/WebM). Пустая conclusion удаляет закрывашку. + UpdateScenarioConclusion(request: UpdateScenarioConclusionReq): Promise; + DownloadScenarioArchive(request: DownloadScenarioArchiveReq): Promise; + UploadScenarioArchive(request: googleapi_HttpBody): Promise; + AddGame(request: AddGameReq): Promise; + GetGames(request: GetGamesReq): Promise; + GetGame(request: GetGameReq): Promise; + // HTTP uploads are streamed by a dedicated handler, bypassing gRPC message limits. + ImportGame(request: googleapi_HttpBody): Promise; + // Dedicated HTTP handlers stream ZIPs; gRPC retains its message size limit. + DownloadGameResultsPackage(request: GameResultsPackageReq): Promise; + ApplyGameResultsPackage(request: ApplyGameResultsPackageReq): Promise; + // Each request creates a fresh setup package, replacing any unfinished preparation. + StartGameExport(request: StartGameExportReq): Promise; + // Release the transfer without discarding ready packages or their future results. + ResetGameLocalMode(request: ResetGameLocalModeReq): Promise; + GetGameExport(request: GetGameExportReq): Promise; + IssueGameTickets(request: IssueGameTicketsReq): Promise; + GetTicket(request: GetTicketReq): Promise; + AcceptTicket(request: AcceptTicketReq): Promise; + // Создать сессию экрана. Экран получает токен и показывает QR для привязки. + CreateScreenSession(request: CreateScreenSessionReq): Promise; + // Получить состояние экрана по его токену. Вызывается самим экраном. + GetScreenState(request: GetScreenStateReq): Promise; + GetGameScreens(request: GetGameScreensReq): Promise; + GetGameMusic(request: GetGameMusicReq): Promise; + SetGameMusic(request: SetGameMusicReq): Promise; + AdvanceScreenMusic(request: AdvanceScreenMusicReq): Promise; + // Привязать экран к игре. Требует авторизацию организатора, автора или администратора. + BindScreen(request: BindScreenReq): Promise; + // Отвязать экран от игры. Требует авторизацию организатора, автора или администратора. + UnbindScreen(request: UnbindScreenReq): Promise; + SetScreenSound(request: SetScreenSoundReq): Promise; + GetGameMap(request: GetGameMapReq): Promise; + UpdateGameMapItem(request: UpdateGameMapItemReq): Promise; + // Изменить общий режим показа всех экранов игры. + SetScreenMode(request: SetScreenModeReq): Promise; + SetScreenTimer(request: SetScreenTimerReq): Promise; + // Отправить общую команду экранам игры. + SendScreenCommand(request: SendScreenCommandReq): Promise; + GetScreenPlan(request: GetScreenPlanReq): Promise; + SaveScreenPlan(request: SaveScreenPlanReq): Promise; + SetScreenPlanStep(request: SetScreenPlanStepReq): Promise; + GetScreenResults(request: GetScreenResultsReq): Promise; + DownloadGamePrintMaterials(request: GetGameReq): Promise; + PreviewTeamReceipt(request: TeamReceiptReq): Promise; + PrintTeamReceipt(request: TeamReceiptReq): Promise; + DownloadTeamQR(request: GetGameReq): Promise; + GetGameAnswers(request: GetGameAnswersReq): Promise; + GetGameResults(request: GetGameResultsReq): Promise; + CreateGameReview(request: CreateGameReviewReq): Promise; + GetGameReviews(request: GetGameReviewsReq): Promise; + GetScenarioReviews(request: GetScenarioReviewsReq): Promise; + GetGameReviewsForModeration(request: GetGameReviewsReq): Promise; + GetReviewsForModeration(request: GetReviewsForModerationReq): Promise; + ModerateGameReview(request: ModerateGameReviewReq): Promise; + UpdateGame(request: UpdateGameReq): Promise; + StartGame(request: StartGameReq): Promise; + PauseGame(request: PauseGameReq): Promise; + PlayGame(request: PlayGameReq): Promise; + FinishGame(request: FinishGameReq): Promise; + ResetGame(request: ResetGameReq): Promise; + DeleteGame(request: DeleteGameReq): Promise; + AddTeam(request: AddTeamReq): Promise; + UpdateTeam(request: UpdateTeamReq): Promise; + DeleteTeam(request: DeleteTeamReq): Promise; + GiveTeamApplications(request: GiveTeamApplicationsReq): Promise; + GetTeamStory(request: GetTeamStoryReq): Promise; + AddTeamAction(request: AddTeamActionReq): Promise; + DeleteLastTeamAction(request: DeleteLastTeamActionReq): Promise; + GetTeamAnswers(request: GetTeamAnswersReq): Promise; + SubmitTeamAnswers(request: SubmitTeamAnswersReq): Promise; + SetTeamAnswerScore(request: SetTeamAnswerScoreReq): Promise; +} + +type RequestType = { + path: string; + method: string; + body: string | null; +}; + +type RequestHandler = (request: RequestType, meta: { service: string, method: string }) => Promise; + +export function createEveningDetectiveServerClient( + handler: RequestHandler +): EveningDetectiveServer { + return { + DeviceHeartbeat(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/devices/heartbeat`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "DeviceHeartbeat", + }) as Promise; + }, + GetDevices(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/devices`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + if (request.gameID) { + queryParams.push(`gameID=${encodeURIComponent(request.gameID.toString())}`) + } + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetDevices", + }) as Promise; + }, + GetTasks(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/tasks`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetTasks", + }) as Promise; + }, + GetTaskAssignees(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/tasks/assignees`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetTaskAssignees", + }) as Promise; + }, + CreateTask(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/tasks`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "CreateTask", + }) as Promise; + }, + UpdateTask(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/tasks/${request.ID}`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "UpdateTask", + }) as Promise; + }, + SetTaskStatus(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/tasks/${request.ID}/status`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "SetTaskStatus", + }) as Promise; + }, + DeleteTask(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/tasks/${request.ID}/delete`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "DeleteTask", + }) as Promise; + }, + GetParticipation(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/participation/get`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "GetParticipation", + }) as Promise; + }, + AnswerParticipation(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/participation/answer`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "AnswerParticipation", + }) as Promise; + }, + Ping(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/test/ping`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "Ping", + }) as Promise; + }, + Echo(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/test/echo`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + if (request.text) { + queryParams.push(`text=${encodeURIComponent(request.text.toString())}`) + } + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "Echo", + }) as Promise; + }, + Signup(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/auth/signup`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "Signup", + }) as Promise; + }, + RefreshPassword(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/auth/refresh-password`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "RefreshPassword", + }) as Promise; + }, + Login(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/auth/login`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "Login", + }) as Promise; + }, + Refresh(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/auth/refresh`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "Refresh", + }) as Promise; + }, + DeleteAccount(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/auth/delete-account`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "DELETE", + body, + }, { + service: "EveningDetectiveServer", + method: "DeleteAccount", + }) as Promise; + }, + GetTerms(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/terms`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetTerms", + }) as Promise; + }, + GetPrivacy(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/privacy`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetPrivacy", + }) as Promise; + }, + GetConsent(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/consent`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetConsent", + }) as Promise; + }, + GetUsers(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/users`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetUsers", + }) as Promise; + }, + GetUserByID(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/users/${request.ID}`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetUserByID", + }) as Promise; + }, + GetMe(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/users/me`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetMe", + }) as Promise; + }, + ChangeUserPassword(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/users/${request.ID}/password`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "ChangeUserPassword", + }) as Promise; + }, + AddUserRole(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/users/${request.ID}/role/add`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "AddUserRole", + }) as Promise; + }, + DeleteUserRole(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/users/${request.ID}/role/delete`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "DeleteUserRole", + }) as Promise; + }, + GetPermissions(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/ui/permissions`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetPermissions", + }) as Promise; + }, + UploadFile(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/files/upload`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "UploadFile", + }) as Promise; + }, + DownloadFile(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.filename) { + throw new Error("missing required field request.filename"); + } + const path = `api/files/${request.filename}`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "DownloadFile", + }) as Promise; + }, + GetPlaylists(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/playlists`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetPlaylists", + }) as Promise; + }, + CreatePlaylist(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/playlists`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "CreatePlaylist", + }) as Promise; + }, + UpdatePlaylist(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/playlists/${request.ID}`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "UpdatePlaylist", + }) as Promise; + }, + DeletePlaylist(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/playlists/${request.ID}`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "DELETE", + body, + }, { + service: "EveningDetectiveServer", + method: "DeletePlaylist", + }) as Promise; + }, + UploadPlaylistTrack(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.playlistID) { + throw new Error("missing required field request.playlistID"); + } + const path = `api/playlists/${request.playlistID}/tracks`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "UploadPlaylistTrack", + }) as Promise; + }, + DeletePlaylistTrack(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.playlistID) { + throw new Error("missing required field request.playlistID"); + } + if (!request.trackID) { + throw new Error("missing required field request.trackID"); + } + const path = `api/playlists/${request.playlistID}/tracks/${request.trackID}`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "DELETE", + body, + }, { + service: "EveningDetectiveServer", + method: "DeletePlaylistTrack", + }) as Promise; + }, + SetScenarioPlaylist(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/scenarios/${request.ID}/playlist`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "SetScenarioPlaylist", + }) as Promise; + }, + AddScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/scenario`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "AddScenario", + }) as Promise; + }, + GetMyScenarios(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/my-scenarios`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetMyScenarios", + }) as Promise; + }, + GetScenariosCatalog(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/catalog`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetScenariosCatalog", + }) as Promise; + }, + GetFullScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/scenarios/${request.ID}/full`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetFullScenario", + }) as Promise; + }, + GetScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/scenarios/${request.ID}`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetScenario", + }) as Promise; + }, + UpdateScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/scenarios/${request.ID}`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "UpdateScenario", + }) as Promise; + }, + PublicScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/scenarios/${request.ID}/public`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "PublicScenario", + }) as Promise; + }, + DraftScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/scenarios/${request.ID}/draft`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "DraftScenario", + }) as Promise; + }, + DeleteScenario(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/scenarios/${request.ID}`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "DELETE", + body, + }, { + service: "EveningDetectiveServer", + method: "DeleteScenario", + }) as Promise; + }, + AddScenarioPlace(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/scenarios/${request.ID}/places`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "AddScenarioPlace", + }) as Promise; + }, + UpdateScenarioPlace(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + if (!request.code) { + throw new Error("missing required field request.code"); + } + const path = `api/scenarios/${request.ID}/places/${request.code}`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "UpdateScenarioPlace", + }) as Promise; + }, + DeleteScenarioPlace(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + if (!request.code) { + throw new Error("missing required field request.code"); + } + const path = `api/scenarios/${request.ID}/places/${request.code}`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "DELETE", + body, + }, { + service: "EveningDetectiveServer", + method: "DeleteScenarioPlace", + }) as Promise; + }, + AddScenarioQuestion(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/scenarios/${request.ID}/questions`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "AddScenarioQuestion", + }) as Promise; + }, + UpdateScenarioQuestion(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + if (!request.code) { + throw new Error("missing required field request.code"); + } + const path = `api/scenarios/${request.ID}/questions/${request.code}`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "UpdateScenarioQuestion", + }) as Promise; + }, + DeleteScenarioQuestion(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + if (!request.code) { + throw new Error("missing required field request.code"); + } + const path = `api/scenarios/${request.ID}/questions/${request.code}`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "DELETE", + body, + }, { + service: "EveningDetectiveServer", + method: "DeleteScenarioQuestion", + }) as Promise; + }, + UpdateScenarioBackground(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/scenarios/${request.ID}/background`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "UpdateScenarioBackground", + }) as Promise; + }, + UpdateScenarioIntro(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/scenarios/${request.ID}/introduction`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "UpdateScenarioIntro", + }) as Promise; + }, + UpdateScenarioConclusion(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/scenarios/${request.ID}/conclusion`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "UpdateScenarioConclusion", + }) as Promise; + }, + DownloadScenarioArchive(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/scenarios/${request.ID}/archive`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "DownloadScenarioArchive", + }) as Promise; + }, + UploadScenarioArchive(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/scenarios/archive`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "UploadScenarioArchive", + }) as Promise; + }, + AddGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/games`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "AddGame", + }) as Promise; + }, + GetGames(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/games`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetGames", + }) as Promise; + }, + GetGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/games/${request.ID}`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetGame", + }) as Promise; + }, + ImportGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/games/import`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "ImportGame", + }) as Promise; + }, + DownloadGameResultsPackage(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/games/${request.ID}/results-package/download`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "DownloadGameResultsPackage", + }) as Promise; + }, + ApplyGameResultsPackage(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/games/${request.ID}/results-package/apply`; // eslint-disable-line quotes + const body = JSON.stringify(request?.package ?? {}); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "ApplyGameResultsPackage", + }) as Promise; + }, + StartGameExport(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/games/${request.ID}/export`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "StartGameExport", + }) as Promise; + }, + ResetGameLocalMode(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/games/${request.ID}/local-mode/reset`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "ResetGameLocalMode", + }) as Promise; + }, + GetGameExport(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/games/${request.ID}/export`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetGameExport", + }) as Promise; + }, + IssueGameTickets(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/tickets`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "IssueGameTickets", + }) as Promise; + }, + GetTicket(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.token) { + throw new Error("missing required field request.token"); + } + const path = `api/tickets/${request.token}`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetTicket", + }) as Promise; + }, + AcceptTicket(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.token) { + throw new Error("missing required field request.token"); + } + const path = `api/tickets/${request.token}/accept`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "AcceptTicket", + }) as Promise; + }, + CreateScreenSession(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/screens`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "CreateScreenSession", + }) as Promise; + }, + GetScreenState(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/screens/state`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "GetScreenState", + }) as Promise; + }, + GetGameScreens(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/screens`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetGameScreens", + }) as Promise; + }, + GetGameMusic(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/music`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetGameMusic", + }) as Promise; + }, + SetGameMusic(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/music`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "SetGameMusic", + }) as Promise; + }, + AdvanceScreenMusic(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/screens/music/advance`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "AdvanceScreenMusic", + }) as Promise; + }, + BindScreen(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.screenID) { + throw new Error("missing required field request.screenID"); + } + const path = `api/screens/${request.screenID}/bind`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "BindScreen", + }) as Promise; + }, + UnbindScreen(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.screenID) { + throw new Error("missing required field request.screenID"); + } + const path = `api/screens/${request.screenID}/unbind`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "UnbindScreen", + }) as Promise; + }, + SetScreenSound(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.screenID) { + throw new Error("missing required field request.screenID"); + } + const path = `api/screens/${request.screenID}/sound`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "SetScreenSound", + }) as Promise; + }, + GetGameMap(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/map`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetGameMap", + }) as Promise; + }, + UpdateGameMapItem(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + if (!request.kind) { + throw new Error("missing required field request.kind"); + } + if (!request.itemID) { + throw new Error("missing required field request.itemID"); + } + const path = `api/games/${request.gameID}/map/items/${request.kind}/${request.itemID}`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "UpdateGameMapItem", + }) as Promise; + }, + SetScreenMode(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/screens/mode`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "SetScreenMode", + }) as Promise; + }, + SetScreenTimer(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/screens/timer`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "SetScreenTimer", + }) as Promise; + }, + SendScreenCommand(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/screens/command`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "SendScreenCommand", + }) as Promise; + }, + GetScreenPlan(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/screen-plan`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetScreenPlan", + }) as Promise; + }, + SaveScreenPlan(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/screen-plan`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "SaveScreenPlan", + }) as Promise; + }, + SetScreenPlanStep(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/screen-plan/step`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "SetScreenPlanStep", + }) as Promise; + }, + GetScreenResults(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/screens/results`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "GetScreenResults", + }) as Promise; + }, + DownloadGamePrintMaterials(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/games/${request.ID}/print-materials`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "DownloadGamePrintMaterials", + }) as Promise; + }, + PreviewTeamReceipt(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + if (!request.teamID) { + throw new Error("missing required field request.teamID"); + } + const path = `api/games/${request.gameID}/teams/${request.teamID}/receipt.png`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "PreviewTeamReceipt", + }) as Promise; + }, + PrintTeamReceipt(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + if (!request.teamID) { + throw new Error("missing required field request.teamID"); + } + const path = `api/games/${request.gameID}/teams/${request.teamID}/receipt/print`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "PrintTeamReceipt", + }) as Promise; + }, + DownloadTeamQR(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/games/${request.ID}/teams/qr.pdf`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "DownloadTeamQR", + }) as Promise; + }, + GetGameAnswers(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/answers`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetGameAnswers", + }) as Promise; + }, + GetGameResults(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/results`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetGameResults", + }) as Promise; + }, + CreateGameReview(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/reviews`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "CreateGameReview", + }) as Promise; + }, + GetGameReviews(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/games/${request.gameID}/reviews`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetGameReviews", + }) as Promise; + }, + GetScenarioReviews(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.scenarioID) { + throw new Error("missing required field request.scenarioID"); + } + const path = `api/scenarios/${request.scenarioID}/reviews`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetScenarioReviews", + }) as Promise; + }, + GetGameReviewsForModeration(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.gameID) { + throw new Error("missing required field request.gameID"); + } + const path = `api/admin/games/${request.gameID}/reviews`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetGameReviewsForModeration", + }) as Promise; + }, + GetReviewsForModeration(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/admin/game-reviews`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetReviewsForModeration", + }) as Promise; + }, + ModerateGameReview(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/admin/game-reviews/${request.ID}`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "ModerateGameReview", + }) as Promise; + }, + UpdateGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/games/${request.ID}`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "UpdateGame", + }) as Promise; + }, + StartGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/games/${request.ID}/start`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "StartGame", + }) as Promise; + }, + PauseGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/games/${request.ID}/pause`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "PauseGame", + }) as Promise; + }, + PlayGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/games/${request.ID}/play`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "PlayGame", + }) as Promise; + }, + FinishGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/games/${request.ID}/finish`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "FinishGame", + }) as Promise; + }, + ResetGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/games/${request.ID}/reset`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "ResetGame", + }) as Promise; + }, + DeleteGame(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/games/${request.ID}`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "DELETE", + body, + }, { + service: "EveningDetectiveServer", + method: "DeleteGame", + }) as Promise; + }, + AddTeam(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + const path = `api/teams`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "AddTeam", + }) as Promise; + }, + UpdateTeam(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/teams/${request.ID}`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "UpdateTeam", + }) as Promise; + }, + DeleteTeam(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/teams/${request.ID}`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "DELETE", + body, + }, { + service: "EveningDetectiveServer", + method: "DeleteTeam", + }) as Promise; + }, + GiveTeamApplications(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/teams/${request.ID}/applications`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "GiveTeamApplications", + }) as Promise; + }, + GetTeamStory(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/teams/${request.ID}/story`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + if (request.password) { + queryParams.push(`password=${encodeURIComponent(request.password.toString())}`) + } + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetTeamStory", + }) as Promise; + }, + AddTeamAction(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/teams/${request.ID}/actions`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "AddTeamAction", + }) as Promise; + }, + DeleteLastTeamAction(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/teams/${request.ID}/last-actions`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "DELETE", + body, + }, { + service: "EveningDetectiveServer", + method: "DeleteLastTeamAction", + }) as Promise; + }, + GetTeamAnswers(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/teams/${request.ID}/answers`; // eslint-disable-line quotes + const body = null; + const queryParams: string[] = []; + if (request.password) { + queryParams.push(`password=${encodeURIComponent(request.password.toString())}`) + } + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "GET", + body, + }, { + service: "EveningDetectiveServer", + method: "GetTeamAnswers", + }) as Promise; + }, + SubmitTeamAnswers(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + const path = `api/teams/${request.ID}/answers`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "POST", + body, + }, { + service: "EveningDetectiveServer", + method: "SubmitTeamAnswers", + }) as Promise; + }, + SetTeamAnswerScore(request) { // eslint-disable-line @typescript-eslint/no-unused-vars + if (!request.ID) { + throw new Error("missing required field request.ID"); + } + if (!request.questionCode) { + throw new Error("missing required field request.questionCode"); + } + const path = `api/teams/${request.ID}/answers/${request.questionCode}/score`; // eslint-disable-line quotes + const body = JSON.stringify(request); + const queryParams: string[] = []; + let uri = path; + if (queryParams.length > 0) { + uri += `?${queryParams.join("&")}` + } + return handler({ + path: uri, + method: "PUT", + body, + }, { + service: "EveningDetectiveServer", + method: "SetTeamAnswerScore", + }) as Promise; + }, + }; +} + +// @@protoc_insertion_point(typescript-http-eof) diff --git a/electron/online.test.ts b/electron/online.test.ts index b4a28a7..9abab47 100644 --- a/electron/online.test.ts +++ b/electron/online.test.ts @@ -188,9 +188,9 @@ async function fixture(t: TestContext, secure = true) { if (init?.method === 'POST') { const body = JSON.parse(String(init.body)) as Record const state = objectMusic(20, Number(body.trackIndex ?? 0)) - state.playback = Number(body.playback ?? 1) model.remote.music = { ...state, + playback: body.playback, volume: body.volume, loop: body.loop, positionMs: body.positionMs, @@ -1160,12 +1160,12 @@ test('локальные режим, музыка и таймер публику f.controller.output(true) await f.online.poll() await f.online.planCommand({ action: 'mode', mode: 3 }) - assert.equal(f.model.remote.mode, 3) + assert.equal(f.model.remote.mode, 'SCREEN_MODE_INTRO_DONE') await f.online.planCommand({ action: 'timer-set', label: 'Отдых', durationSeconds: 60 }) assert.equal(f.model.remote.timerDurationSeconds, 60) await f.online.planCommand({ action: 'play', channel: 'music' }) - const music = f.model.remote.music as { playback: number } - assert.equal(music.playback, 2) + const music = f.model.remote.music as { playback: string } + assert.equal(music.playback, 'MUSIC_PLAYBACK_PLAYING') const operation = f.controller.state.channels.music.operation await f.online.poll() assert.equal(f.controller.state.channels.music.operation, operation) diff --git a/electron/online.ts b/electron/online.ts index 5017d76..b5b1401 100644 --- a/electron/online.ts +++ b/electron/online.ts @@ -1,3 +1,5 @@ +import { createEveningDetectiveServerClient } from './api/generated/crabs/evening_detective_server/index.js' +import { apiScreenMode, apiScreenCommand, apiPlan } from './api-values.js' import { ApiError, NetworkError, SessionChangedError, networkFailure } from './request-error.js' import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises' import { join } from 'node:path' @@ -449,12 +451,9 @@ export class Online { const version = this.planVersion this.lastPlan = Date.now() try { - const result = await this.api( - `/api/games/${pkg.package.game.id}/screen-plan`, - undefined, - true, - AbortSignal.timeout(5000), - ) + const result = await this.client(true, AbortSignal.timeout(5000)).GetScreenPlan({ + gameID: pkg.package.game.id, + }) await this.acceptPlan(parsePlan(result.plan), pkg.key, generation, version) } catch (error) { if (generation === this.generation && version === this.planVersion) @@ -472,13 +471,10 @@ export class Online { const version = ++this.planVersion this.planBusy++ try { - const result = await this.json( - await this.authorized(`/api/games/${pkg.package.game.id}/screen-plan`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(plan), - }), - ) + const result = await this.client().SaveScreenPlan({ + gameID: pkg.package.game.id, + ...apiPlan(plan), + }) if (generation !== this.generation) throw new SessionChangedError() await this.acceptPlan(parsePlan(result.plan), pkg.key, generation, version) } finally { @@ -576,12 +572,10 @@ export class Online { return } if (intent.action === 'step') { - const result = await this.api( - `/api/games/${pkg.package.game.id}/screen-plan/step`, - { currentStep: intent.index }, - true, - AbortSignal.timeout(5000), - ) + const result = await this.client(true, AbortSignal.timeout(5000)).SetScreenPlanStep({ + gameID: pkg.package.game.id, + currentStep: intent.index, + }) const confirmed = parsePlan(result.plan) const shown = this.controller.state.catalog.steps[intent.index] await this.acceptPlan(confirmed, pkg.key, generation, version) @@ -599,29 +593,29 @@ export class Online { } } else if ('channel' in intent && intent.channel === 'music') { const c = localState.channels.music - await this.api( - `/api/games/${pkg.package.game.id}/music`, - { - playlistID: c.playlistId ?? 0, - trackIndex: c.trackIndex, - positionMs: Math.max(0, Math.round(c.position * 1000)), - volume: Math.round(c.volume), - loop: c.loop, - playback: c.desired === 'playing' ? 2 : c.desired === 'paused' ? 3 : 1, - }, - true, - AbortSignal.timeout(5000), - ) + await this.client(true, AbortSignal.timeout(5000)).SetGameMusic({ + gameID: pkg.package.game.id, + playlistID: c.playlistId ?? 0, + trackIndex: c.trackIndex, + positionMs: Math.max(0, Math.round(c.position * 1000)), + volume: Math.round(c.volume), + loop: c.loop, + playback: + c.desired === 'playing' + ? 'MUSIC_PLAYBACK_PLAYING' + : c.desired === 'paused' + ? 'MUSIC_PLAYBACK_PAUSED' + : 'MUSIC_PLAYBACK_STOPPED', + }) } else if (intent.action === 'mode' || intent.action === 'select') { - await this.api( - `/api/games/${pkg.package.game.id}/screens/mode`, - { mode: localState.scene.mode }, - true, - AbortSignal.timeout(5000), - ) + await this.client(true, AbortSignal.timeout(5000)).SetScreenMode({ + gameID: pkg.package.game.id, + mode: apiScreenMode(localState.scene.mode), + }) } else if (intent.action === 'text') { - const path = `/api/games/${pkg.package.game.id}/screen-plan` - const current = parsePlan((await this.api(path)).plan) + const current = parsePlan( + (await this.client().GetScreenPlan({ gameID: pkg.package.game.id })).plan, + ) if ( generation !== this.generation || session !== this.controller.state.session || @@ -637,13 +631,7 @@ export class Online { timerLabel: '', timerDurationSeconds: 0, }) - await this.json( - await this.authorized(path, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(current), - }), - ) + await this.client().SaveScreenPlan({ gameID: pkg.package.game.id, ...apiPlan(current) }) } if ( generation !== this.generation || @@ -651,7 +639,10 @@ export class Online { version !== this.planVersion ) return - const result = await this.api(`${path}/step`, { currentStep: index }) + const result = await this.client().SetScreenPlanStep({ + gameID: pkg.package.game.id, + currentStep: index, + }) await this.acceptPlan(parsePlan(result.plan), pkg.key, generation, version) if ( generation !== this.generation || @@ -661,21 +652,18 @@ export class Online { return this.controller.markStep(index) } else if (intent.action === 'timer-set') { - await this.api( - `/api/games/${pkg.package.game.id}/screens/timer`, - { label: intent.label, durationSeconds: intent.durationSeconds }, - true, - AbortSignal.timeout(5000), - ) + await this.client(true, AbortSignal.timeout(5000)).SetScreenTimer({ + gameID: pkg.package.game.id, + label: intent.label, + durationSeconds: intent.durationSeconds, + }) } else { const index = this.controller.state.scene.stepIndex if (index !== null && index !== this.plan.currentStep) { - const result = await this.api( - `/api/games/${pkg.package.game.id}/screen-plan/step`, - { currentStep: index }, - true, - AbortSignal.timeout(5000), - ) + const result = await this.client(true, AbortSignal.timeout(5000)).SetScreenPlanStep({ + gameID: pkg.package.game.id, + currentStep: index, + }) await this.acceptPlan(parsePlan(result.plan), pkg.key, generation, version) if ( generation !== this.generation || @@ -699,16 +687,12 @@ export class Online { } if (!(intent.action in commands)) throw new Error('Некорректная команда плана') expectedCommand = commands[intent.action as keyof typeof commands] - await this.api( - `/api/games/${pkg.package.game.id}/screens/command`, - { - command: expectedCommand, - positionMs: 'position' in intent ? Math.round(intent.position * 1000) : 0, - volume: 'volume' in intent ? intent.volume : this.controller.state.channels.main.volume, - }, - true, - AbortSignal.timeout(5000), - ) + await this.client(true, AbortSignal.timeout(5000)).SendScreenCommand({ + gameID: pkg.package.game.id, + command: apiScreenCommand(expectedCommand), + positionMs: 'position' in intent ? Math.round(intent.position * 1000) : 0, + volume: 'volume' in intent ? intent.volume : this.controller.state.channels.main.volume, + }) } if ( generation !== this.generation || @@ -719,12 +703,10 @@ export class Online { this.planNotice = '' // Ответ следующего опроса становится базой: собственная команда не применяется дважды. if (this.screen) { - const result = await this.api( - '/api/screens/state', - { screenID: this.screen.id, accessToken: this.screen.token }, - false, - AbortSignal.timeout(5000), - ) + const result = await this.client(false, AbortSignal.timeout(5000)).GetScreenState({ + screenID: this.screen.id, + accessToken: this.screen.token, + }) if (generation !== this.generation || session !== this.controller.state.session) return const remote = parseRemote(result.state) if (remote.gameId !== pkg.package.game.id) throw new Error('Привязка экрана изменилась') @@ -748,12 +730,9 @@ export class Online { needsMainSync = !isMusicCommand } if (needsMainSync && remote.command === 0) { - const result = await this.api( - `/api/games/${pkg.package.game.id}/screen-plan`, - undefined, - true, - AbortSignal.timeout(5000), - ) + const result = await this.client(true, AbortSignal.timeout(5000)).GetScreenPlan({ + gameID: pkg.package.game.id, + }) await this.acceptPlan(parsePlan(result.plan), pkg.key, generation, version) if ( generation !== this.generation || @@ -1021,19 +1000,18 @@ export class Online { if (this.refreshPromise) return this.refreshPromise const c = this.credentials! this.refreshPromise = (async () => { - const response = await this.raw(c.server, '/api/auth/refresh', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ refreshToken: c.refresh }), - }) - if (generation !== this.generation) throw new SessionChangedError() - if (response.status === 401) { - this.credentials = null - this.state.authenticated = false - await this.persistSecrets() - throw new ApiError(401) + let result + try { + result = await this.client(false, undefined, c.server).Refresh({ refreshToken: c.refresh }) + } catch (error) { + if (generation !== this.generation) throw new SessionChangedError() + if (error instanceof ApiError && error.status === 401) { + this.credentials = null + this.state.authenticated = false + await this.persistSecrets() + } + throw error } - const result = await this.json(response) if (generation !== this.generation) throw new SessionChangedError() this.credentials = { ...c, @@ -1070,21 +1048,19 @@ export class Online { } return response } - private async api(path: string, body?: object, authorized = true, signal?: AbortSignal) { - const init: RequestInit = - body === undefined - ? { signal } - : { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - signal, - } - return this.json( - authorized - ? await this.authorized(path, init) - : await this.raw(this.state.server, path, init), - ) + private client(authorized = true, signal?: AbortSignal, server = this.state.server) { + return createEveningDetectiveServerClient(async (request) => { + const init: RequestInit = { + method: request.method, + headers: { 'Content-Type': 'application/json', Accept: 'application/json' }, + body: request.body, + signal, + } + const path = `/${request.path}` + return this.json( + authorized ? await this.authorized(path, init) : await this.raw(server, path, init), + ) + }) } async act(input: unknown): Promise { const data = object(input) @@ -1127,12 +1103,7 @@ export class Online { this.state.games = [] await this.persistSecrets() this.state.server = server - const response = await this.raw(server, '/api/auth/login', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email, password }), - }) - const result = await this.json(response) + const result = await this.client(false, undefined, server).Login({ email, password }) this.credentials = { server, access: token(result.accessToken), @@ -1140,7 +1111,7 @@ export class Online { screen: null, } try { - const me = object((await this.api('/api/users/me')).user) + const me = object((await this.client().GetMe({})).user) if ( !Array.isArray(me.roles) || !me.roles.some((r) => ['admin', 'organizer', 'author'].includes(String(r))) @@ -1261,17 +1232,13 @@ export class Online { this.state.packagesRevision++ await this.saveSelection() } - const result = await this.api('/api/screens', {}, false) + const result = await this.client(false).CreateScreenSession({}) const s: ScreenSession = { id: String(integer(result.screenID, 1)), token: token(result.accessToken), gameId, } - await this.api(`/api/screens/${s.id}/bind`, { - screenID: s.id, - accessToken: s.token, - gameID: s.gameId, - }) + await this.client().BindScreen({ screenID: s.id, accessToken: s.token, gameID: s.gameId }) this.screen = s this.state.screenId = s.id this.state.connection = 'waiting' @@ -1285,10 +1252,7 @@ export class Online { case 'sound': { const generation = this.generation if (!this.screen) throw new Error('Сначала подключите управление') - await this.api(`/api/screens/${this.screen.id}/sound`, { - screenID: this.screen.id, - soundEnabled: true, - }) + await this.client().SetScreenSound({ screenID: this.screen.id, soundEnabled: true }) if (generation === this.generation) this.state.soundEnabled = true return } @@ -1299,7 +1263,7 @@ export class Online { } } private async loadGames() { - const result = await this.api('/api/games') + const result = await this.client().GetGames({}) if (!Array.isArray(result.games ?? [])) throw new Error('Некорректный список игр') this.state.games = ((result.games ?? []) as unknown[]).map((value) => { const g = object(value) @@ -1328,11 +1292,10 @@ export class Online { this.resultsBusy = true this.lastResults = Date.now() try { - const result = await this.api( - '/api/screens/results', - { screenID: s.id, accessToken: s.token }, - false, - ) + const result = await this.client(false).GetScreenResults({ + screenID: s.id, + accessToken: s.token, + }) if (generation !== this.generation || key !== this.controller.state.catalog.key) return if (!Array.isArray(result.questions ?? []) || !Array.isArray(result.teams ?? [])) throw new Error('Некорректные результаты') @@ -1378,12 +1341,10 @@ export class Online { const version = this.planVersion this.pollBusy = true try { - const result = await this.api( - '/api/screens/state', - { screenID: s.id, accessToken: s.token }, - false, - AbortSignal.timeout(5000), - ) + const result = await this.client(false, AbortSignal.timeout(5000)).GetScreenState({ + screenID: s.id, + accessToken: s.token, + }) if (generation !== this.generation || version !== this.planVersion) return let next = parseRemote(result.state) if (next.gameId !== s.gameId) { @@ -1458,12 +1419,10 @@ export class Online { await this.synchronize() return } - const result = await this.api( - '/api/screens/music/advance', - { screenID: this.screen.id, accessToken: this.screen.token }, - false, - AbortSignal.timeout(5000), - ) + const result = await this.client(false, AbortSignal.timeout(5000)).AdvanceScreenMusic({ + screenID: this.screen.id, + accessToken: this.screen.token, + }) if (generation !== this.generation || report.session !== this.controller.state.session) return const music = parseRemote({ music: result.state }).music if (this.received) { diff --git a/electron/screen-package.ts b/electron/screen-package.ts index ef3583a..fc09560 100644 --- a/electron/screen-package.ts +++ b/electron/screen-package.ts @@ -1,3 +1,4 @@ +import { createEveningDetectiveServerClient } from './api/generated/crabs/evening_detective_server/index.js' import { ApiError, NetworkError, @@ -240,10 +241,18 @@ export async function prepareScreenPackage( AbortSignal.timeout(30_000), ...(request.signal ? [request.signal] : []), ]) - async function get(suffix: string): Promise { + const client = createEveningDetectiveServerClient(async (requestData, meta) => { + const suffix = + meta.method === 'GetGameMusic' + ? '/music' + : meta.method === 'GetScreenPlan' + ? '/screen-plan' + : '' let response: Response try { - response = await fetchAPI(`${server}/api/games/${request.gameId}${suffix}`, { + response = await fetchAPI(`${server}/${requestData.path}`, { + method: requestData.method, + body: requestData.body, headers: { Authorization: `Bearer ${request.token}`, Accept: 'application/json' }, credentials: 'omit', redirect: 'error', @@ -292,8 +301,12 @@ export async function prepareScreenPackage( request.onRequestFailure?.({ path: `/api/games/{id}${suffix}`, reason: 'INVALID_JSON' }) throw new Error('Некорректный JSON в ответе API') } - } - const [game, plan, music] = await Promise.all([get(''), get('/screen-plan'), get('/music')]) + }) + const [game, plan, music] = await Promise.all([ + client.GetGame({ ID: request.gameId }), + client.GetScreenPlan({ gameID: request.gameId }), + client.GetGameMusic({ gameID: request.gameId }), + ]) return buildScreenPackage(server, request.gameId, { game, plan, music }) } diff --git a/eslint.config.mjs b/eslint.config.mjs index 7e39ec7..f111556 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -3,7 +3,16 @@ import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescri import skipFormatting from '@vue/eslint-config-prettier/skip-formatting' export default defineConfigWithVueTs( - { ignores: ['dist/**', 'dist-electron/**', 'release/**', 'scripts/**', '*.mjs'] }, + { + ignores: [ + 'electron/api/generated/**', + 'dist/**', + 'dist-electron/**', + 'release/**', + 'scripts/**', + '*.mjs', + ], + }, vue.configs['flat/essential'], vueTsConfigs.recommended, skipFormatting, diff --git a/package.json b/package.json index 399ddf2..e37b943 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "acceptance": "npm run build && node scripts/start.cjs scripts/smoke.cjs --manual", "build:electron": "node -e \"require('node:fs').rmSync('dist-electron', { recursive: true, force: true })\" && tsc -p tsconfig.electron.json", "smoke:packaged": "node scripts/packaged-smoke.cjs", - "verify:release": "node scripts/release-finish.cjs" + "verify:release": "node scripts/release-finish.cjs", + "proto:gen": "node scripts/proto-gen.cjs" }, "dependencies": { "vue": "^3.5.0", diff --git a/scripts/online-smoke.cjs b/scripts/online-smoke.cjs index 7857287..cde5fe6 100644 --- a/scripts/online-smoke.cjs +++ b/scripts/online-smoke.cjs @@ -90,9 +90,13 @@ module.exports = async function onlineSmoke({ } if (path === '/api/games/6/screens/command') { currentCommand = body.command - if (body.command === 8 || body.command === 10) timerState = 2 - if (body.command === 9) timerState = 3 - if (body.command === 11) timerState = 1 + if ( + body.command === 'SCREEN_COMMAND_TIMER_START' || + body.command === 'SCREEN_COMMAND_TIMER_RESUME' + ) + timerState = 2 + if (body.command === 'SCREEN_COMMAND_TIMER_PAUSE') timerState = 3 + if (body.command === 'SCREEN_COMMAND_TIMER_RESET') timerState = 1 version++ return send({}) } diff --git a/scripts/proto-gen.cjs b/scripts/proto-gen.cjs new file mode 100644 index 0000000..74328f2 --- /dev/null +++ b/scripts/proto-gen.cjs @@ -0,0 +1,44 @@ +const { execFileSync } = require('node:child_process') +const { existsSync, mkdirSync, mkdtempSync, cpSync, rmSync } = require('node:fs') +const { delimiter, join, resolve } = require('node:path') +const { tmpdir } = require('node:os') +const root = resolve(__dirname, '..') +const plugin = (process.env.PATH || '') + .split(delimiter) + .map((p) => + join( + p, + process.platform === 'win32' + ? 'protoc-gen-typescript-http.exe' + : 'protoc-gen-typescript-http', + ), + ) + .find(existsSync) +if (!plugin) throw new Error('Install protoc-gen-typescript-http v0.10.0; see README') +if (execFileSync('protoc', ['--version'], { encoding: 'utf8' }).trim() !== 'libprotoc 36.0') + throw new Error('protoc 36.0 is required') +if ( + !execFileSync('go', ['version', '-m', plugin], { encoding: 'utf8' }).includes( + 'go.einride.tech/protoc-gen-typescript-http\tv0.10.0\t', + ) +) + throw new Error('protoc-gen-typescript-http v0.10.0 is required') +const source = resolve(root, '../evening_detective_server/api') +const temporary = mkdtempSync(join(tmpdir(), 'screen-proto-')) +try { + execFileSync( + 'protoc', + [ + `--plugin=protoc-gen-typescript-http=${plugin}`, + `--proto_path=${source}`, + `--typescript-http_out=${temporary}`, + join(source, 'main.proto'), + ], + { stdio: 'inherit' }, + ) + const target = join(root, 'electron/api/generated') + mkdirSync(target, { recursive: true }) + cpSync(temporary, target, { recursive: true }) +} finally { + rmSync(temporary, { recursive: true, force: true }) +}