---
title: API
---
[TOC]
----
# 1. Идентификатор клиента
Каждый клиент имеет идентификатор в виде UUIDv4, который он генерирует при первом запуске приложения и запоминает.
# 2. Данные о сессии
О сессии на сервере хранится, как минимум, следующее:
1. ID клиента-организатора
2. список игроков:
- никнейм
- ID клиента
- готовность
3. код сессии
4. полные параметры игры
# 3. Создание сессии
1. Создатель сессии (клиент) нажимает на кнопку.
2. Клиент отсылает серверу POST-запрос.
3. Сервер генерирует код сессии *случайным образом* (не последовательно) и отправляет клиенту этот код, сохранив данные сессии в хранилище.
4. После этого клиент подключается к сессии.
# 4. Подключение к сессии
1. Взаимодействие в пределах сессии происходит по вебсокету.
2. Установив websocket-соединение, клиент отсылает сообщение `Join`.
3. Дальнейшие действия — согласно протоколу ниже.
# 5. Протокол сессии
Протокол работает поверх вебсокетов и состоит из набора дискретных сообщений (потоковой передачи не предусмотрено).
Каждое сообщение протокола — один фрейм вебсокета.
Так как вебсокеты работают по TCP и TLS, гарантируются доставка, целостность и очерёдность.
Путь к вебсокету содержит в себе версию протокола.
Если сервер ещё или больше не поддерживает версию, общения не состоится (можно выслать 404).
Иначе используется та версия, которая указана клиентом.
Далее описывается первая версия протокола.
## 5.1. Метаязык описания схемы
Структура сообщений описывается метаязыком, состоящим из последовательности определений типов:
```
имя = тип
```
Комментарии начинаются с `#` и завершаются концом строки.
Далее указаны типы данных, используемые в описании сообщений.
`iN` — знаковое N-битное число.
Например, `i32`.
- В JSON — целое число.
Вещественное число допускается не принимать.
`uN` — беззнаковое N-битное число.
Например, `u32`.
- В JSON — целое число без знака минуса.
Отрицательные числа недопустимы.
Вещественное число допускается не принимать.
`f64` — вещественное число двойной точности (`double`).
`NaN` и бесконечные значения, в том числе из-за переполнения, недопустимы.
`bool` — булево значение.
`str` — строка в UTF-8.
`uuid` — UUIDv4.
- В JSON — строка стандартного формата: `xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
Допустимы значения, в которых поля варианта и подтипа UUID не соответствуют UUIDv4.
`time` — 64-битное показание каких-либо часов в миллисекундах.
В качестве типов могут быть использованы литеральные значения (`0`, `-8`, `"привет"`, `null`), которые должны присутствовать в JSON.
`[] тип` — массив элементов типа `тип`.
```
{
поле₁: тип₁
поле₂?: тип₂
…
}
```
Структурный тип с полями `поле₁`, `поле₂`, …, соответственно имеющими типы `тип₁`, `тип₂`, ….
Если после названия поля следует `?`, это поле может быть опущено.
```
{
..тип₀
поле₁: тип₁
поле₂?: тип₂
…
}
```
Расширение структурного типа `тип₀`, состояющее из всех его полей, а также нижеперечисленных.
Таким образом поля также могут быть переопределены.
```
| тип₁
| тип₂
…
```
Тип-объединение.
Значением этого типа может быть любое, которое допустимо каким-либо из входящих в объединение типов.
Другие значения недопустимы.
Сокращённая запись в одну строку: `тип₁ | тип₂ | …`.
## 5.2. Формат сообщений
Формат сообщений — JSON (для удобства отладки).
```
MessageKind =
| "error" # ошибка
| "join" # подключение к сессии
| "joined" # успешное подключение к сессии
| "game-status" # информация о текущей игре
| "ready" # сигнал готовности к игре
| "kick" # выбрасывание игрока из сессии организатором
| "leave" # покидание сессии
| "task-start" # начало задания
| "task-answer" # ввод ответа на задание
| "poll-start" # начало голосования
| "poll-choose" # выбор варианта в голосовании
| "task-end" # завершение голосования
| "game-end" # завершение игровой сессии
| "game-start" # начало игры
| "waiting" # ожидание игроков в лобби
MessageId = u32
BaseMessage = {
msg-id: MessageId
kind: MessageKind
time: time
}
```
Каждое сообщение имеет 2 обязательных поля:
- `msg-id`: идентификатор сообщения.
Задаётся отправителем произвольно.
Используется, чтобы связать запрос с ответом (в ответе будет указан `msg-id`).
Клиенту допускается отправлять несколько (в том числе различных) сообщений с одним `msg-id`, но ответы не получится соотнести.
Сервер обязан использовать для различных сообщений различные `msg-id`.
- `kind`: тип сообщения.
- `time`: текущее показание монотонных часов отправителя.
Используется сервером для коррекции отметок времени в сообщениях с полями типа `time`.
### 5.2.1. Коррекция времени
В полях типа `time` различных сообщений сервер должен учесть разницу между показаниями часов на сервере и на клиенте.
Делается это следующим образом:
1. Каждое сообщение $m_i$ клиента содержит поле `BaseMessage.time`.
2. При получении сообщения от клиента сервер запоминает собственное время $t_i$.
3. Сервер вычисляет разницу $Δt_i$ между $t$ и `BaseMessage.time`.
4. Высчитывается экспоненциально взвешенное среднее $Δt$ разниц моментов времени $Δt_i$.
5. При отправлении сообщения с полем `time` сервер вычитает разницу $Δt$ из исходного момента времени.
Если сетевой лаг не имеет больших выбросов, $Δt$ будет отражать примерное время между отправкой сообщения клиентом и получением его сервером.
## 5.3. Ошибки
Если получатель принимает ошибочное сообщение, он отвечает на него сообщением `Error`.
```
# общие ошибки, допустимые в ответ на любое сообщение
GeneralErrorKind =
| "internal" # внутренняя ошибка отправителя
| "malformed-msg" # недопустимый формат сообщения
| "proto-violation" # нарушение протокола
# ошибки в ответ на Join
JoinErrorKind =
| "session-expired" # сессия уже завершилась
| "lobby-full" # сессия переполнена
| "nickname-used" # выбранный никнейм уже кем-то используется
# ошибки в ответ на привилегированные действия
OpErrorKind =
| "op-only" # запрашиваемое действие требует привилегий организатора
# ошибки во время игры
GameErrorKind =
| "inactivity" # клиент удаляется из-за неактивности
| "session-closed" # организатор сессии покидает её до начала игры
ErrorKind =
| GeneralErrorKind
| JoinErrorKind
| OpErrorKind
| GameErrorKind
Error = {
..BaseMessage
ref-id: MessageId | null
kind: ErrorKind
message: str
}
```
Поля:
- `ref-id`: `msg-id` у сообщения, которое привело к ошибке.
Если ошибка не спровоцирована каким-либо сообщением, равняется `null`.
- `kind`: код ошибки.
- `message`: диагностическая информация об ошибке произвольного формата.
Сообщение `Error` посылается, если:
1. Это явно указано в протоколе.
2. Если получено WebSocket-сообщение, которое не удалось декодировать (не JSON, отсутствуют поля, поля имеют недопустимые значения).
Код ошибки — `malformed-msg`.
3. Если получено сообщение, которое, будучи корректным синтаксически, не предусмотрено протоколом.
Например, сервер не может отправить клиенту сообщение `Join`, и получение клиентом такого сообщения приводит к этой ошибке.
Код ошибки — `proto-violation`.
4. По инициативе отправителя с кодом `internal`.
Протокол не накладывает ограничений на причины для этой ошибки.
5. После начала игры клиент в двух заданиях не устанавливает готовность ответа.
Код ошибки — `inactivity`.
Дальнейшие действия те же, что и в случае отправки клиентом сообщения `Leave`.
Все ошибки являются фатальными.
После отправки ошибки отправитель закрывает соединение.
## 5.4. Состояния
Соединение в зависимости от фазы игры пребывает в одном из состояний.
Каждое состояние определяет набор сообщений, которые допустимо отправлять по собственной инициативе.
В дополнение к этому, cообщение `Error` клиент или сервер может отправить в любом из состояний.
> Например, в начальном состоянии допустимо клиентское сообщение `Join`.
> Никаких других сообщений ни сервер, ни клиент без запроса послать не могут.
> В то же время после получения `Join` сервер может отправить `Joined`, так как делает это он не по собственной инициативе, а в ответ на запрос клиента.
>
> Таким образом, все допустимые сообщения:
> - `Error`
> - допустимые состоянием
> - допустимые ответы на отправленные запросы
### 5.4.1. Начальное
Первое состояние, в котором пребывает соединение сразу после установления WebSocket-соединения.
#### Допустимые сообщения клиента
- `Join`: для входа в сессию.
### 5.4.2. Ожидание в лобби
Ожидание игроков в лобби.
#### Допустимые сообщения сервера
- `GameStatus`: при изменениях в составе игроков.
- `Waiting`: при изменениях в готовности игроков.
- `GameStart`: начало игры.
#### Допустимые сообщения клиента
- `Ready`: подтверждение готовности к игре.
- `Kick`: удаление организатором игрока из сессии.
- `Leave`: покидание сессии.
### 5.4.3. Начало игры
Состояние сразу после начала игры, во время которого происходит обратный отсчёт.
#### Допустимые сообщения сервера
- `TaskStart`: начало первого задания.
#### Допустимые сообщения клиента
- `Leave`: покидание игры.
### 5.4.4. Выполнение задания
Игроки выполняют задания, пока не истечёт время.
#### Допустимые сообщения сервера
- `PollStart`: начало голосования, если оно предусмотрено форматом задания.
- `TaskEnd`: просмотр итогов задания, если голосования форматом задания не предусмотрено.
#### Допустимые сообщения клиента
- `TaskAnswer`: обновление ответа на задание.
- `PollChoose`: выбор варианта в голосовании к предыдущему заданию (сообщение игнорируется сервером).
- `Leave`: покидание игры.
### 5.4.5. Голосование
Если форматом задания предусмотрено голосование, во время его проведения соединение пребывает в этом состоянии.
#### Допустимые сообщения сервера
- `TaskEnd`: просмотр итогов задания.
#### Допустимые сообщения клиента
- `TaskAnswer`: обновление ответа на предыдущее задание (сообщение игнорируется сервером).
- `PollChoose`: выбор варианта в голосовании.
- `Leave`: покидание игры.
### 5.4.6. Просмотр итогов задания
Промежуток игры, во время которого игроки просматривают результаты выполненного задания.
#### Допустимые сообщения сервера
- `TaskStart`: начало следующего задания.
- `GameEnd`: завершение игры, если задание было последним.
#### Допустимые сообщения клиента
- `TaskAnswer`: обновление ответа на предыдущее задание (сообщение игнорируется сервером).
- `PollChoose`: выбор варианта в голосовании к предыдущему заданию (сообщение игнорируется сервером).
- `Leave`: покидание игры.
## 5.5. Сообщение Join
```
Nickname = str
Join = {
..BaseMessage
kind: "join"
nickname: Nickname
}
```
Отправляется клиентом для входа в игровую сессию.
Поля:
- `nickname`: выбранный ник в игре.
В случае успеха сервер добавляет клиента в сессию, генерируя для него новый идентификатор игрока, и отправляет сообщение `Joined`.
Если игрок с таким `client-id` уже присутствует в лобби, то сервер отправляет `Joined`, в котором указывает первоначально сгенерированный идентификатор игрока и данный клиентом ранее ник.
Этот механизм используется для переподключения клиента.
Если сессия недействительна, то сервер отправляет `Error` с кодом ошибки `session-expired`.
Если ник уже используется кем-то из игроков, то сервер отправляет `Error` с кодом ошибки `nickname-used`.
Если сессия переполнена, то сервер отправляет `Error` с кодом ошибки `lobby-full`.
Если игра уже началась, и игрок не присутствовал в составе на момент её начала, был удалён организатором во время ожидания либо сервером во время игры из-за неактивности, то сервер отправляет `Error` с кодом ошибки `unknown-session`.
## 5.6. Сообщение Joined
```
PlayerId = u32
SessionId = uuid
GameDetails = {
..BaseGameInfo
tasks: [] Task
}
Joined = {
..BaseMessage
kind: "joined"
ref-id: MessageId
player-id: PlayerId
session-id: SessionId
game: GameDetails
}
```
[Описание Task](#Описание-Task). [Описание BaseGameInfo](#GameInfo).
Отправляет сервер при успешном (пере-)заходе клиента в сессию.
После него сервер сразу же отправляет `GameStatus` и одно из `Waiting`, `GameStart`, `TaskStart`, `PollStart`, `TaskEnd` — в зависимости от текущей фазы игры.
Клиент затем переходит в соответствующее полученному сообщению состояние.
Поля:
- `ref-id`: `msg-id` сообщения `Join`.
- `player-id`: идентификатор игрока в пределах сессии.
- `session-id`: идентификатор сессии.
- `game`: информация об игре.
## 5.7. Сообщение GameStatus
```
PlayerList = [] {
player-id: PlayerId
nickname: Nickname
}
GameStatus = {
..BaseMessage
kind: "game-status"
players: PlayerList
}
```
Отправляется сервером сразу после `Joined` при заходе в сессию, а также при изменении состава игроков во время ожидания.
Поля:
- `players`: текущий список игроков в сессии.
Содержит, как минимум, информацию о клиенте.
- `player-id`: идентификатор игрока в пределах сессии.
- `nickname`: никнейм этого игрока.
Если это сообщение отправлено при заходе клиента в сессию, сервер дальше посылает одно из следующих сообщений:
- `Waiting` — во время ожидания игроков в лобби;
- `GameStart` — во время обратного отсчёта перед началом игры;
- `TaskStart` — во время выполнения одного из заданий;
- `PollStart` — во время голосования за ответы на задание;
- `TaskEnd` — во время подведения итогов выполненного задания.
## 5.8. Сообщение Ready
```
Ready = {
..BaseMessage
kind: "ready"
ready: bool
}
```
Отправляется клиентом во время ожидания игры для уведомления о своей готовности (или неготовности).
Поля:
- `ready`: статус готовности игрока.
При изменении статус готовности сервер отправляет всем игрокам сообщение `Waiting` с обновлённым списком.
При этом, если статус не изменился, `Waiting` не отправляется, то есть сообщение является идемпотентным.
Если сессия не требует подтверждения готовности, отправка этого сообщения клиентом-организатором запускает игру.
Если сессия требует подтверждения готовности, игра запускается после того, как все участники сессии выразят готовность.
## 5.9. Сообщение Kick
```
Kick = {
..BaseMessage
kind: "kick"
player-id: PlayerId
}
```
Сообщение отправляется клиентом-организатором, чтобы удалить какого-либо игрока из сессии.
Поля:
- `player-id`: идентификатор удаляемого игрока.
В случае успеха сервер выполняет те же действия, что и при получении им от удаляемого игрока сообщения `Leave`.
> В частности, организатор может кикнуть себя, и сессия завершится.
Если клиент не является организатором, сервер отправляет `Error` с кодом ошибки `op-only`.
Если указанный игрок не присутствует в сессии, сообщение игнорируется.
## 5.10. Сообщение Leave
```
Leave = {
..BaseMessage
kind: "leave"
}
```
Отправляется клиентом, чтобы выйти из сессии.
Сервер, получив это сообщение, удаляет игрока из сессии.
При этом:
- Во время ожидания игроков:
- Если сессию покидает организатор, то сервер рассылает всем игрокам сообщение `Error` с кодом ошибки `session-closed`.
- Иначе сервер рассылает всем игрокам `GameStatus`, а затем, если игрок ранее выразил готовность, также `Waiting`.
- Во время выполнения задания удаляется ответ игрока, если он был дан.
В дальнейших списках (варианты для голосования, scoreboard и т. д.) он не указывается.
- Во время голосования удаление игрока откладывается до завершения фазы подведения итогов.
> Вариант ответа этого игрока продолжает присутствовать в списке для голосования, ему начисляются баллы за ответ, если он победил, и он включается в список `TaskEnd.scoreboard`.
- Во время просмотра итогов задания удаление игрока откладывается до завершения этой фазы.
После получения сообщения сервер разрывает WebSocket-соединение с клиентом, даже если удаление отложено.
## 5.11. Сообщение TaskStart
```
# варианты для заданий с ответом в виде выбора варианта
# допустимые значения устанавливаются требованиями
TaskOption = str
TaskStart = {
..BaseMessage
kind: "task-start"
task-idx: u8
deadline: time
# присутствует только для заданий с типом ответа в виде выбора варианта
options?: []TaskOption
# присутствует только для заданий с типом ответа в виде фото
img-uri?: str
}
```
Отправляется сервером при начале выполнения очередного задания, а также перезашедшему игроку после `GameStatus`.
Поля:
- `task-idx`: номер задания в игре (начинается с 0).
- `deadline`: время завершения выполнения задания.
Сервер корректирует время согласно отметкам `time` в предыдущих сообщениях клиента.
- `options`: список вариантов ответа для заданий этого формата.
- `img-uri`: ссылка для загрузки и получения изображения-ответа.
Клиент переходит в состояние выполнения задания.
## 5.12. Сообщение TaskAnswer
```
# допустимые значения устанавливаются требованиями
CheckedWordAnswer = str
WordAnswer = str
# допустимые значения: от 0 до |TaskStart.options| - 1 включительно.
TaskOptionIndex = u8
Answer =
| CheckedWordAnswer # для заданий с ответом в виде текста с проверкой
| WordAnswer # для заданий с ответом в виде текста
| TaskOptionIndex # для заданий с ответом в виде выбора варианта
TaskAnswer = {
..BaseMessage
kind: "task-answer"
task-idx: u8
ready: bool
answer?: Answer
}
```
Отправляется клиентом при изменении ответа во время выполнения задания.
Поля:
- `task-idx`: номер задания, на которое даётся ответ (тот же, что в `TaskStart.task-idx`).
- `ready`: статус готовности ответа.
- `answer`: новое значение ответа.
Если отсутствует, значение не изменяется.
Для заданий с ответом в виде фото эти изображения загружаются на отдельный эндпоинт апи по ссылке `TaskStart.img-uri`, а сообщение используется только для уведомления о готовности.
Если сервер получает `TaskAnswer` после завершения задания, сообщение игнорируется.
Если `task-idx` указывает на задание, которое ещё не начиналось в ходе игры, сервер отвечает сообщением `Error` с кодом ошибки `malformed-msg`.
Для задания в формате выбора ответа этим ответом указывается индекс выбранного варианта в `TaskStart.options`.
При успехе сервер обновляет статус готовности ответа и, если оно присутствует, само его значение.
## 5.13. Сообщение PollStart
```
# При выборе слова — варианты слов.
# При выборе фото — URL на фото.
PollOption = str
Poll = {
..BaseMessage
kind: "poll-start"
task-idx: u8
deadline: time
options: []PollOption
}
```
Отправляется сервером при начале голосования (если оно предусмотрено форматом задания).
Поля:
- `task-idx`: номер задания (тот же, что в `TaskStart.task-idx`).
- `deadline`: время завершения голосования.
Сервер корректирует время согласно отметкам `time` в предыдущих сообщениях клиента.
- `options`: варианты для голосования.
Клиент переходит в состояние голосования.
## 5.14. Сообщение PollChoose
```
PollChoose = {
..BaseMessage
kind: "poll-choose"
task-idx: u8
option-idx: u8 | null
}
```
Посылается клиентом во время голосования.
Поля:
- `task-idx`: номер задания (тот же, что в `PollStart.task-idx`).
- `option-idx` — индекс выбранного варианта в `PollStart.options`.
Равен `null` для отмены выбора.
Если сервер получает это сообщение после завершения голосования за это задание, сообщение игнорируется.
Если `task-idx` указывает на задание, голосование за которое ещё не начиналось в ходе игры, сервер отвечает сообщением `Error` с кодом ошибки `malformed-msg`.
При успехе сервер фиксирует выбранный вариант.
## 5.15. Сообщение TaskEnd
```
Points = u32
TaskScoreboard = [] {
player-id: PlayerId
task-points: Points
total-points: Points
}
TaskAnswers =
# для заданий с ответом в виде текста с проверкой
| [] {
value: CheckedWordAnswer # значение ответа
player-count: u16 # число игроков, которые ввели этот текст
correct: bool # является ли ответ верным
}
# для заданий с ответом в виде фото
| [] {
value: str # ссылка на изображение-ответ
votes: u16 # число голосов за ответ
}
# для заданий с ответом в виде текста
| [] {
value: WordAnswer # значение ответа
votes: u16 # число голосов за ответ
}
# для заданий с ответом в виде выбора варианта
| [] {
value: TaskOption # значение ответа
player-count: u16 # число игроков, которые выбрали этот вариант
correct: bool # является ли ответ верным
}
TaskEnd = {
..BaseMessage
kind: "task-end"
task-idx: u8
deadline: time
scoreboard: TaskScoreboard
answers: TaskAnswers
}
```
Отсылается сервером по завершении задания.
Поля:
- `task-idx`: номер задания (тот же, что в `TaskStart.task-idx`).
- `deadline`: время завершения просмотра результатов и начала следующего задания.
Сервер корректирует время согласно отметкам `time` в предыдущих сообщениях клиента.
- `scoreboard`: список игроков с начисленными им баллами за задание и счётом за всю игру.
Отсортирован по убыванию начисленных за это задание баллов.
- `answers`: список ответов на задание.
Клиент переходит в состояние просмотра итогов задания.
## 5.16. Сообщение GameEnd
```
# отсортирован по убыванию
Scoreboard = [] {
player-id: PlayerId
total-points: Points
}
GameEnd = {
..BaseMessage
kind: "game-end"
scoreboard: Scoreboard
}
```
Отсылается сервером по завершении игры.
После отправки сообщения сессия завершается, и все WebSocket-соединения закрываются.
Поля:
- `scoreboard`: отсортированный по убыванию очков список игроков и их счёт.
## 5.17. Сообщение GameStart
```
GameStart = {
..BaseMessage
kind: "game-start"
deadline: time
}
```
Посылается сервером при запуске игры.
Поля:
- `deadline`: время начала выполнения первого задания игры.
Сервер корректирует время согласно отметкам `time` в предыдущих сообщениях клиента.
Клиент переходит в состояние начала игры.
## 5.18. Сообщение Waiting
```
Waiting = {
..BaseMessage
kind: "waiting"
ready: [] PlayerId
}
```
Отправляется сервером во время ожидания игроков сразу после `GameStatus`, а также при изменениях в готовности игроков.
Поля:
- `ready`: список идентификаторов игроков, выразивших готовность к игре.
Клиент переходит в состояние ожидания в лобби.
# 6. Загрузка изображений
Изображения загружаются POST-запросом на соответствующий эндпоинт API.
При успехе сервер возвращает код загрузки, который можно использовать в качестве ответа.
# 7. Игры и задания
1. Игры и задания получаются и управляются через ряд эндпоинтов.
2. У каждой игры и каждого задания есть уникальный идентификатор некоторого формата.
3. Отдельный эндпоинт каталога выдаёт список идентификаторов стандартных игр.
4. Для редактирования параметров игры/заданий нужна аутентификация.
Она происходит по ID клиента.
5. Все игры доступны для чтения по идентификатору, но ответы показываются только создателю игры.
# 8. API версии 1
Все эндпоинты API с данной версией начинаются с префикса `/api/v1`.
## 8.1. Формат базового ответа
Ответ сервера — в формате JSON (кроме картинок).
При успешной обработке запроса содержимое ответа определяется конкретным эндпоинтом. В случае ошибки ответ будет содержать в теле объект `ApiErrorResponse`, позволяющий лучше идентифицировать проблему.
Ошибки более подробно описаны в секции [8.3 Ошибки API](#8.3.-Ошибки-API).
## 8.2. Используемые типы
```rust
UserId = uuid
GameId = uuid
TaskId = uuid
ImageId = uuid
SessionId = uuid
InviteCode = [A-Z0-9]{6}
# название игры и описание игры
# с ограничениями согласно требованиям
GameName = str
GameDesc = str
Date = str # в формате "yyyy-mm-dd"
PngImage = bytes # изображение в формате .png
JpegImage = bytes # изображение в формате .jpeg
Image =
| PngImage
| JpegImage
ImageRequest = i8
ImageRequestResponse = {
img-request: ImageRequest
img-uri: str
}
```
## 8.3. Ошибки API
```rust
AuthorizationErrorKind =
| "user-id-invalid"
| "only-owner-allowed"
| "auth-required"
| "not-enough-privileges" # если обычный пользователь пытается воспользоваться Admin API
ImageErrorKind =
| "img-not-provided"
| "img-too-large"
| "img-format-unsupported"
| "img-malformed"
| "img-upload-forbidden" # изображение заблокировано для изменения
InvalidParamErrorKind =
| "schema-invalid" # тело запроса не соответствует спецификации
| "param-missing" # отсутствует необходимый query параметр
| "param-invalid"
InvalidTaskInfoErrorKind =
| "task-not-found"
| "task-invalid"
InvalidGameInfoErrorKind =
| "game-invalid"
GeneralErrorKind =
| "not-found"
ApiErrorKind =
| AuthorizationErrorKind
| ImageErrorKind
| InvalidParamErrorKind
| InvalidGameInfoErrorKind
| GeneralErrorKind
ApiErrorResponse = {
error: ApiErrorKind
message: str
}
```
При возникновении ошибки сервер отправляет ошибочный ответ, указывая код ошибки.
## 8.4. Общие положения по HTTP
### 8.4.1. Response code
| Код | HTTP-статус | Причина |
|:-----:|:--------------------- |:------------------------------------------------------------ |
| `500` | Internal Server Error | в ходе обработки запроса произошла ошибка на стороне сервера |
| `405` | Method Not Allowed | метод нельзя применять к текущему ресурсу |
### 8.4.2. Аутентификация
Аутентификация происходит с помощью заголовка `Authorization`.
Формат:
```http
Authorization: Bearer <user-id>
```
Здесь `<user-id>` — значение типа `UserId`.
Например:
```http
Authorization: Bearer 12345678-abcd-efab-cdef-123456789012
```
Для каждого запроса указано, является ли аутентификация обязательной.
Запросы, отмеченные как Admin API,
#### Ошибки
- отсутствие заголовка `Authorization` в запросе, который требует аутентификации
- код ответа: `401 Unauthorized`
- ошибка: `auth-required`
- невалидный `user-id`
- код ответа: `401 Unauthorized`
- ошибка: user-id-invalid
- ресурс доступен только создателю
- код ответа: `403 Forbidden`
- ошибка: `only-owner-allowed`
- пользователь пытается выполнить функцию Admin API
- код ответа: `403 Forbidden`
- ошибка: `not-enough-privileges`
## 8.5. Игры
Префикс: `/api/v1/games`.
### 8.5.1. Получение списка игр из каталога
**GET** `/api/v1/games`
**Поддерживает аутентификацию**
Получение списка доступных игр из каталога.
#### Виды ответов
- Успех — `200 OK` и список игр в теле.
#### Response body
```rust
{
games: [] IdGameInfo
}
```
Содержит список игр в неопределённом порядке.
##### GameInfo
```rust
BaseGameInfo = {
name: GameName
description: str
img-uri: str
date-changed: Date
}
IdGameInfo = {
..BaseGameInfo
id: GameId
tasks: [] TaskWithId
}
```
[Описание TaskWithId](#Описание-Task)
### 8.5.2. Получение информации об игре
**GET** `/api/v1/games/{game-id}`
**Требует аутентификации**
Получение информации о конкретной игре по её идентификатору.
#### Path params
- `game-id` (тип `GameId`).
#### Виды ответов
- Успех — `200 OK`.
- Некорректный запрос — `404 Not Found`.
- `not-found` — игра с данным id не найдена.
#### Response body
Экземпляр `GameInfo` c id = `game-id`:
[Описание GameInfo](#GameInfo)
### 8.5.3. (Admin API) Публикация игры
**POST** `/api/v1/games`
**Требует аутентификации**
Создание новой игры на сервере.
#### Request body
```rust!
{
name: GameName
description: str
img-request: ImageRequest
task-ids: [] TaskId
}
```
#### Виды ответов
- Успех — `201 Created`.
- Некорректный запрос — `400 Bad Request`.
- `schema-invalid` — нарушение схемы запроса.
- `game-invalid` — информация об игре некорректна.
- `task-invalid` — информация о задании некорректна.
#### Response body
```rust
{
game-id: GameID
img-requests: [] ImageRequestResponse
}
```
### 8.5.4. (Admin API) Обновление игры
**PUT** `/api/v1/games/{game-id}`
**Требует аутентификации**
Обновление информации об игре, созданной клиентом.
#### Path params
- `game-id` (тип `GameId`).
#### Request body
```rust!
{
name: GameName
description: str
img-request: ImageRequest
task-ids: [] TaskId
}
```
#### Виды ответов
- Успех — `200 OK`.
- Некорректный запрос — `400 Bad Request`.
- `schema-invalid` — нарушение схемы запроса.
- `game-invalid` — информация об игре некорректна.
- `task-invalid` — информация о задании некорректна.
- Некорректный запрос — `404 Not Found`.
- `not-found` — игра с данным id не найдена.
#### ResponseBody
```rust!
{
img-requests: [] ImageRequestResponse
}
```
### 8.5.5. (Admin API) Удаление игры
**DELETE** `/api/v1/games/{game-id}`
**Требует аутентификации**
Удаление созданной пользователем игры.
#### Path params
- `game-id` (тип `GameID`).
#### Виды ответов
- Успех — `204 No Content`.
- Некорректный запрос — `404 Not Found`.
- `not-found` — игра с данным id не найдена
## 8.6. Задания
Префикс - `/api/v1/tasks`
```rust!
FixedDuration = {
kind: "fixed"
secs: u16
}
DynamicDuration = {
kind: "dynamic"
secs: u16
}
PollDuration = FixedDuration | DynamicDuration
BaseTask = {
name: str
description: str
duration: FixedDuration # длительность выполнения задания
}
PollTask = {
..BaseTask
poll-duration: PollDuration # длительность голосования
}
```
#### Описание Task
```rust!
PhotoTask = {
..PollTask
type: "photo"
}
TextTask = {
..PollTask
type: "text"
}
CheckedTextTask = {
..BaseTask
type: "checked-text"
}
ChoiceTask = {
..BaseTask
type: "choice"
}
Task =
| PhotoTask
| TextTask
| CheckedTextTask
| ChoiceTask
TaskWithImage = {
..Task
img-uri: str
}
AnsweredCheckedTextTask = {
..CheckedTextTask
answer: str
}
AnsweredChoiceTask {
..ChoiceTask
options: [] str
answer-idx: u8
}
AnsweredTask =
| PhotoTask
| TextTask
| AnsweredCheckedTextTask
| AnsweredChooseOptionTask
AnsweredTaskWithImage = {
..AnsweredTask
img-uri: str
}
AnsweredTaskWithImageRequest = {
..AnsweredTask
img-request: ImageRequest
}
TaskWithId = {
..TaskWithImage
id: TaskId
last-updated: Date
}
AnsweredTaskWithId = {
..AnsweredTaskWithImage
id: TaskId
last-updated: Date
}
```
### 8.6.1. Получение заданий каталога
**GET** `/api/v1/tasks`
**Требует аутентификации**
Получение всех заданий из каталога.
#### Виды ответов
- Успех — `200 OK`.
#### Response body
```rust!
{
tasks: [] AnsweredTaskWithId
}
```
[Описание AnsweredTaskWithId](#Описание-Task)
### 8.6.2. (Admin API) Добавление задания
**POST** `/api/v1/tasks`
**Требует аутентификации**
Загрузка задания на сервер.
#### Request body
Экземпляр `AnsweredTaskWithImageRequest`.
[Описание AnsweredTaskWithImageRequest](#Описание-Task)
#### Виды ответов
- Успех — `200 OK`.
- Некорректный запрос — `400 Bad Request`.
- `schema-invalid` — нарушение схемы запроса.
- `task-invalid` — информация о задании некорректна.
#### Response body
```rust!
{
task-id: TaskId
img-requests: [] ImageRequestResponse
}
```
### 8.6.3. Получение задания
**GET** `/api/v1/tasks/{task-id}`
**Требует аутентификации**
Получение задания по его идентификатору.
#### Path params
- `task-id` (тип `TaskId`).
#### Виды ответов
- Успех — `200 OK`.
- Некорректный запрос — `404 Not Found`.
- `task-not-found`
#### Response body
Экземпляр `AnsweredTaskWithId`, соответствующий данному идентификатору.
[Описание AnsweredTaskWithId](#Описание-Task)
### 8.6.4. (Admin API) Обновление задания
**PUT** `/api/v1/tasks/{task-id}`
**Требует аутентификации**
Обновление задания по его идентификатору.
#### Path params
- `task-id` (тип `TaskId`).
#### Request body
Экземпляр `AnsweredTaskWithImageRequest`.
[Описание AnsweredTaskWithImageRequest](#Описание-Task)
#### Виды ответов
- Успех — `200 OK`.
- Некорректный запрос — `400 Bad Request`.
- `schema-invalid` — нарушение схемы запроса.
- `task-invalid` — информация о задании некорректна.
- Некорректный запрос — `404 Not Found`.
- `task-not-found` — задание с данным id не найдено.
#### Response body
```rust!
{
img-requests: [] ImageRequestResponse
}
```
### 8.6.5. (Admin API) Удаление задания
**DELETE** `/api/v1/tasks/{task-id}`
**Требует аутентификации**
Удаления задания по его идентификатору.
#### Path params
- `task-id` (тип `TaskId`).
#### Виды ответов
- Успех — `204 No Content`.
- Некорректный запрос — `404 Not Found`.
- task-not-found — задание с данным id не найдено
## 8.7. Изображения
Префикс — `/api/v1/images`.
При отправке изображения **обязательно** указывается заголовок `Content-Type` с одним из следующих значений:
- `image/png`
- `image/jpeg`
### 8.7.0 Способы защиты
При загрузке нового изображения дополнительно сделать следующее
- запомнить дату и время загрузки: `date-time-upload = NOW()`
- сохранить id создателя: `user-id`
( `NOW()` - возвращает текущие дату и время )
Раз в N минут удалять все изображения у которых:
- `ref-count <= 0 && NOW() - date-time-upload > N`
( `ref-count` - число публичных игр и сессий использующих это изображение )
### 8.7.1. Получение изображения
**GET** `/api/v1/images/{img-id}` (`img-uri` полученный от сервера)
**Требует аутентификации**
Получение изображения по его идентификатору.
#### Path params
- `img-id` (тип `ImageId`).
#### Response code
- Успех — `200 OK`.
- Некорректный `img-uri` — `404 Not Found`.
- `not-found`
- `img-uri` корректен, но изображение не было загружено - `404 Not Found`
- `img-not-provided`
#### Response body
```
Image
```
### 8.7.2. Загрузка изображения на сервер
**PUT** `/api/v1/images/{img-id}` (`img-uri` полученный от сервера)
**Требует аутентификации**
Загрузка изображения на сервер
#### Request body
```
Image
```
#### Виды ответов
- Успех — `204 No Content`.
- Некорректный `img-uri` — `404 Not Found`.
- `not-found`
- Некорректный запрос — `400 Bad Request`.
- `img-too-large` — размер изображения превышает допустимый лимит.
- `img-malformed` — изображение повреждено или не соответствует заявленному формату.
- Неподдерживаемый формат в `Content-Type` — `415 Unsupported Media Type`
- `img-unsupported-format` — формат изображения не поддерживается.
- Изображеие заблокировано для изменения (например, после завершения задания) - `403 Forbidden`
- `img-upload-forbidden`
## 8.8. Игровая сессия
Префикс — `/api/v1/session`.
### 8.8.1. Создание игровой сессии
**POST** `/api/v1/session`
**Требует аутентификации**
Создание новой игровой сессии с последней версией игры.
#### Request body
Экземпляр `CreateSessionRequest`:
```rust!
BaseCreateSessionRequest = {
player-count: u8 # min: 2, max: 20
}
FullGameInfo = {
name: GameName
description: str
img-request: int
tasks: [] AnsweredTaskWithImageRequest
}
PublicCreateSessionRequest = {
..BaseCreateSessionRequest
game-type: "public"
game-id: GameId
}
PrivateCreateSessionRequest = {
..BaseCreateSessionRequest
game-type: "private"
game: FullGameInfo
}
CreateSessionRequest =
| PublicCreateSessionRequest
| PrivateCreateSessionRequest
```
[Описание AnsweredTask](#Описание-Task)
#### Виды ответов
- Успех — `200 OK`.
- Некорректный запрос — `400 Bad Request`.
- `schema-invalid` — нарушение схемы запроса.
- `game-invalid` — информация об игре некорректна.
- `task-invalid` — информация о задании некорректна.
- `invalid-game-id` — при неверном идентификаторе игры.
- `invalid-players-count` — для некорректного значения `players-count`.
#### Response body
```rust
{
session-id: SessionId
img-requests: [] ImageRequestResponse
}
```
В `session-id` указан идентификатор созданной сессии.
### 8.8.2. Подключение к игровой сессии
**GET** `/api/v1/session`
**Требует аутентификации**
Установление WebSocket-соединения для игровой сессии.
Выбор сессии производится либо с помощью параметра `session-id`, содержащий UUID сессии, либо посредством `invite-code`.
Коды приглашения действительны только во время ожидания в лобби; идентификатор сессии действителен в течение всего времени жизни сессии.
#### Query params
- `session-id` (тип `SessionId`).
- `invite-code` (тип `InviteCode`).
#### Виды ответов
- Успех — `101 Switching Protocols`.
Дальнейшее общение — по протоколу сессии.
- Не указан ни `session-id`, ни `invite-code` — `400 Bad Request`.
- `param-missing`
- Неверный либо недействительный `session-id` или `invite-code` — `404 Not Found`.
- `not-found`
- Не указан требуемый для WebSocket заголовок `Upgrade` — `426 Upgrade Required`.
## 8.9. Аккаунт пользователя
Пока аккаунты есть только у администраторов.
Поэтому эти эндпоинты позволяют определить клиенту, является ли он администратором.
### 8.9.1. Получение информации об аккаунте
**GET** `/api/v1/user`
**Требует аутентификации**
Возвращает информацию об аккаунте пользователя с переданным идентификатором клиента.
#### Виды ответов
- Успех — `200 OK`.
#### Response body
Экземпляр `UserInfo`.
```rust
UserRole =
| "user" # обычный пользователь
| "admin" # администратор
UserInfo = {
role: UserRole,
}
```
----
Идеи
- привязываться к айдишникам, которые генерят юзеры сами
- можно подвязать equi-x, если мы боимся спама
- пикчи загружать через апи, отдавать код загрузки и везде его юзать
- нужна ли здесь бд?
----
# Пожелания к разработчикам API
общие пожелания
0. вам нужно (ещё до проработки конкретных ручек) договориться об общем виде апи: пути, схема базового ответа, схема ошибок, способ аутентификации, — чтобы не получилось 3 различных апи на одном сервере — по одному на дизайнера
1. апи должно быть версионировано
2. каждый эндпоинт должен описывать схему принимаемого документа
3. 5xx ошибки в каждом эндпоинте раздувают документ: это надо описать один раз для всех эндпоинтов
4. пнг на серв закинуть можно в 2 форматах: multipart/form-data и image/png — какой имеется в виду?
5. каждый эндпоинт должен описывать схему отдаваемого документа
6. думайте об ошибках:
1. если с апи придёт ошибка, которую надо будет показать на клиенте, она обязательно должна иметь код
2. если разраб накосячит с запросом, он должен получить инфу о том, где косяк, чтоб не мучать бэкэндеров по каждому пустяку
3. клиент должен иметь возможность обработать все ошибки, поэтому делайте их полезными
7. думайте о том, как апи будут юзать:
1. если серв отдаёт список, подумайте, не нужна ли там пагинация?
2. если клиент будет тянуть какие-то данные, стоит подготовить их в одном ответе (список доступных игр должен иметь то, что будет показываться в клиенте, чтобы он не бомбил серв миллиардом последующих запросов)
8. думайте о том, как апи будут реализовывать бэкэндеры:
1. если отдаётся статика, можно ли минимизировать действия сервера? (в идеале иметь возможность позволить нгинху обработать, не трогая бэк)
2. если каждый запрос на частое действие должен будет лезть в БД, делать там джойн десяти таблиц и считать предикаты, нельзя ли урезать отдаваемые данные до необходимого?
9. если объект меняется/удаляется, что будет с теми, кто на него ссылался? лучше не давать возможность удалять, чем забыть починить все убитые ссылки
10. юзайте конвенции реста и семантику хттп:
1. GET, PUT и DELETE должны быть идемпотентны
2. GET не должен модифицировать состояние
3. не стоит разнотипным эндпоинтам давать пересекаться: делать /collection/:id и /collection/create — это не очень хорошая идея
4. добавлять объекты можно через PUT /collection/:id, если id создаётся клиентом, или через POST /collection
5. получить список объектов — GET /collection
11. определите формат всех идшников
1. если идшник будет последовательным числом, вы подумали, что их можно будет перебрать?
12. если серв принимает тяжеловесный контент, предусмотрите механизм ограничения времени хранения
13. GET /collection и POST /collection — два совершенно разных эндпоинта апи, они вряд ли имеют общие параметры, и их нужно описывать в отдельных секциях, как если бы пути были различными
## Желаемые ответы
Как должен выглядеть примерный ответ серва.
Допустим, посылаем `GET /api/v1/server-mood`.
### Ошибка
Если хотите, чтоб серв мог вернуть несколько ошибок:
```json
{
"errors": [
{
"code": "bad-day",
"description": "having a bad day sorry"
},
{
"code": "wrong-week",
"description": "check your calendar",
"week-number": 42
}
],
"message": "could not process your request"
}
```
Если не хотите запарываться (рекомендую, потому что проще делать):
```json
{
"error": "wrong-week",
"description": "check your calendar",
"week-number": 42
}
```
### Успешный ответ
```json
{
"mood": "perfectly happy thanks for asking"
}
```
### Как клиент это будет обрабатывать
Он смотрит на ответ, видит там не 2xx, пытается пропарсить как жсон, ищет там поле `error` (или `errors`, если сервак в апи поддерживает множество ошибок) и обрабатывает ошибки.
Если пришёл 2xx-ответ, он работает с ним согласно описанию эндпоинта (пикчи сохраняет, жсон парсит).
# Где хранить инфу об игре?
И там, и там.
1. При создании сессии надо передавать либо айди публичной игры, либо полное описание своей.
2. Участники в `Joined` получают описание игры (независимо от того, публичная она или нет), включая список заданий без ответов.
3. API-ручки для создания, модификации игр и заданий сделать частью админ-панели.
Обычные юзеры доступа пока иметь не будут.
Версионирования не требуется; отдавать последнюю.
Редактирование и удаление не блокировать.
4. Сервер полную информацию об игре будет хранить вместе с остальными данными сессии.
Если игра публичная, она высасывается из БД (транзакционно), и при сессии хранится копия.
5. Изображения загружаются без привязки к сессии.
При описании игры указывается получаемый идентификатор пикч.
Пикчи удаляются после недели (суток, месяца, как угодно) неиспользования.
Пикча считается использующейся, если есть активная сессия с этой пикчей либо публичная игра ссылается на неё.
6. Админ ли юзер, определяется по идентификатору клиента.
Серв в БД будет хранить табличку админов.
# TODO
- [x] Не делать пагинацию для тех эндпоинтов, где количество отдаваемых ответов небольшое (в списке игр).
- [x] Детализировать загрузку изображений: указать `Content-Type` и пр.
- [x] Расчленить ошибки при загрузке изображений на несколько: слишком большое изображение, неподдерживаемый формат, битое изображение и т. д.
- [x] Отдавать картинку напрямую в теле ответа, без обрамления в JSON.
- [x] Разделить эндпоинты типа `GET /images/:id`, `DELETE /images/:id`, имеющие одинаковые пути, но разные методы, на отдельные.
- [x] Не пихать строки формата в описание ошибок.
Описание ошибок не задавать в принципе, позволить произвольное.
Клиент должен работать только с кодами ошибок.
Если же требуется какая-то дополнительная инфа, её нужно поместить в отдельное поле ошибочного ответа (так, чтобы клиент по коду мог понять, что должны быть дополнительные поля).
- [x] Если поле задано как `name?: [] T`, описать, чем отличается отсутствие `name` от `name = []`.
По возможности ограничить использование опциональных полей в ответах сервера.
Отсутствие данных лучше показывать как `name: T | null`.
- [x] Привести коды ошибок к одному формату, чтобы не было, что рест-апи юзает числа, а вебсокеты — строки.
- [x] Описать способ аутентификации и ошибки авторизации в одном месте.
Если эндпоинт требует авторизации, ссылаться на описанное, не дублировать инфу.
- [x] Чётко отделить ответы ошибок от успешных.
Не слать массив ошибок в успешные ответы.
- [x] Убрать категорию `warn`.
Нефатальные ошибки — сложность на всех сторонах.
- [x] Не юзать числовые идшники.
- [x] Коды ошибок включить прямо в виды ответов.
Например, когда эндпоинт отдаёт 400 из-за некорректного поля, прямо там же нужно код ошибки подписать.
- [x] Посмотрите трезвым взглядом на коды ошибок и выправьте в один стиль.
Не надо `img-damaged` и `too-large-img`: `img` либо в начале, либо в конце.
- [x] Дефолтная сортировка от старых к новым?
- [x] Зачем вообще сортировать игры на стороне сервера, если никакой пагинации нет?
- [x] Если посмотрите на макет, вы увидите, что клиент будет иметь таб со своими играми и таб с играми из каталога.
Каким образом клиент поймёт из текущего ответа, в какой вкладке каждая игра будет расположена?
- [x] Если сервер хочет послать пустой ответ, он не может отправить 200 OK (на этот случай есть 204).
- Рекомендую [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-method-definitions): вся семантика собрана в одном месте.
- [x] 409 Conflict имеет определённый смысл, который не подходит для неуспеха удаления (инфа всё в том же рфц).
- [x] К слову, а как/когда вообще будет эндпоинт удаления картинки вызываться клиентом?
Нам точно он нужен?
- [x] Так как клиент может аутентифицироваться (заголовком) уже при установлении WebSocket-соединения, стоит так его и опознавать.
Протокол сессии я уже сообразно изменил.
Добавьте в эндпоинт требование авторизации.
- [x] Какое тело ответа сервера на 404?
Нужен код ошибки.
- [x] Неверный формат картинки — это 415.
- [x] Нужно добавить описание операций с заданиями.
- [x] Нужно добавить возможность задания динамической времени для задания/голосования.
- [x] Подумать по поводу публикации игр сразу после создания/изменения.
- [x] Решить, как будет происходить передача данных о заданиях в рамках игровой сессии: последовательно по мере выполнения этапов игры или за раз во время начала сессии
- [x] Защита для эндпоинта изображений.
- [x] Сделать всё, как указано в [секции](#%D0%93%D0%B4%D0%B5-%D1%85%D1%80%D0%B0%D0%BD%D0%B8%D1%82%D1%8C-%D0%B8%D0%BD%D1%84%D1%83-%D0%BE%D0%B1-%D0%B8%D0%B3%D1%80%D0%B5).
- [x] Обновить вебсокетный протокол. (для JS)
- [x] Убрать POST для загрузки пикч, оставив только PUT.
- [x] Вместо `img-id` отдавать `img-url`.
- [x] Сказать, что случится при GET ещё не загруженного изображения.
- [x] Блокировать изменение пикчи после завершения выполнения задания, если пикча — ответ на задание с фотками.
- [x] Завести код ошибки, когда клиент лезет на админские эндпоинты.