- убраны клиент бота, транспорт обновлений, отправитель сообщений, сборка входа при старте, секция настроек и зависимость go-telegram-bot-api; из конвейера ушла доставка ответа отправителю — исход виден опросом готовности. Колонки адресата и значение источника остались в схеме: применённые шаги не переписываются - шаг 202608140003 запрещает пустого владельца у аудиозаписи и у файла; существующие строки он не проверяет, и это принято сознательно — искать их надо запросом до выкладки - ревью нашло два пред-существующих дефекта, оба закрыты: пустой второй ответ распознавателя стирал сохранённую расшифровку, а пустая расшифровка перестала быть заметной вместе с убранной доставкой. Попутно поднят golang.org/x/image до v0.45.0 — красный шаг vulns, воспроизводился и на чистом master
246 lines
19 KiB
Markdown
246 lines
19 KiB
Markdown
## MODIFIED Requirements
|
||
|
||
### Requirement: Приём записи по HTTP
|
||
|
||
Сервис SHALL принимать запись от внешней программы запросом `POST /api/audio` с
|
||
телом `multipart/form-data` и полем `audio` **только от узнанного отправителя**.
|
||
Запрос без сессии MUST получать код `401`, и по нему MUST не заводиться ни файл,
|
||
ни аудиозапись. Принятая запись от узнанного отправителя MUST быть сохранена и
|
||
получить заведённую под неё аудиозапись на рубеже `uploaded`; ответ MUST нести
|
||
идентификатор записи полем `job_id` и её рубеж полем `status`.
|
||
|
||
Значение рубежа в ответе изменилось: прежде приём отдавал `created`. Перечень
|
||
состояний назван проектом необратимым, и ломка объявлена прямо — состояние
|
||
теперь называет достигнутое, а не предстоящее, и `created` в новом перечне нет
|
||
вовсе.
|
||
|
||
Имена полей ответа нормативны и MUST остаться прежними: контракт HTTP API
|
||
объявлен проектом необратимым, и переименование поля ломает внешнюю программу
|
||
молча. Меняются значения поля рубежа, а не его имя.
|
||
|
||
Отказ по отсутствию сессии наступает **раньше** чтения тела: запись, за которую
|
||
не заплатит узнанный отправитель, не должна попасть даже в память.
|
||
|
||
Приём не судит о годности записи сам: расширение он берёт из имени файла, а
|
||
пригодность содержимого узнаёт у источника метаданных.
|
||
|
||
Куда именно ложится принятая запись, приёму не принадлежит: раскладку выбирает
|
||
хранилище, и нормирует её capability `storage`.
|
||
|
||
Владельцем принятой записи приём SHALL назначать предъявителя сессии. Обязательность
|
||
владельца при этом MUST держаться и схемой хранилища: колонка владельца пустого
|
||
значения не принимает вовсе, и норму эту держит capability `storage`. Проверка в
|
||
приёме от этого не лишняя — она отвечает отправителю понятным отказом до того, как
|
||
запись попадёт в память, а схема отвечала бы отказом сохранения после укладки
|
||
файла.
|
||
|
||
Предъявитель, чья сессия не даёт учётной записи пользователя, MUST получать
|
||
отказ `403` и MUST получать его **до чтения тела** — там же, где стоит отказ по
|
||
отсутствию сессии. Сессия владельца панели — именно такой случай: узнан он всё
|
||
же узнан, а записи в коллекции пользователей у него нет, и владельцем записи он
|
||
стать не может.
|
||
|
||
Код здесь другой, чем у запроса без сессии, и это не оплошность: `401` значит
|
||
«предъяви себя», а предъявитель себя предъявил. Утечки по разнице кодов нет —
|
||
оба ответа говорят о самом спрашивающем, а не о том, какие записи заведены.
|
||
|
||
Отказ **после** укладки записи потребовал бы убрать уже сохранённый файл, а
|
||
уборки файлов сервис не умеет вовсе: норма, обязывающая к недостижимому, не
|
||
пишется.
|
||
|
||
#### Scenario: Запись принята
|
||
|
||
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
|
||
- **AND** отправитель предъявил сессию
|
||
- **WHEN** программа шлёт `POST /api/audio` с полем `audio`
|
||
- **THEN** ответ имеет код `201`, а в теле лежат непустой `job_id` и `status`
|
||
со значением `uploaded`
|
||
- **AND** содержимое записи целиком лежит в хранилище одним файлом
|
||
- **AND** владельцем заведённой аудиозаписи стоит предъявитель сессии
|
||
|
||
#### Scenario: Сессия не даёт учётной записи пользователя
|
||
|
||
- **GIVEN** предъявлена сессия владельца панели
|
||
- **WHEN** он шлёт `POST /api/audio` с полем `audio`
|
||
- **THEN** ответ имеет код `403`
|
||
- **AND** ни файла, ни аудиозаписи не заводится
|
||
|
||
#### Scenario: Сессии нет
|
||
|
||
- **WHEN** программа шлёт `POST /api/audio` с полем `audio` без сессии
|
||
- **THEN** ответ имеет код `401`
|
||
- **AND** ни файла, ни аудиозаписи не заводится
|
||
- **AND** тело ответа не несёт данных записи
|
||
|
||
#### Scenario: Поля с записью нет
|
||
|
||
- **GIVEN** отправитель предъявил сессию
|
||
- **WHEN** программа шлёт `POST /api/audio` без поля `audio`
|
||
- **THEN** ответ имеет код `400` и сообщение об отсутствии записи
|
||
- **AND** ни файла, ни аудиозаписи не заводится
|
||
|
||
#### Scenario: Размеру записи приём не судья
|
||
|
||
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
|
||
- **AND** отправитель предъявил сессию
|
||
- **WHEN** программа шлёт запись нулевой длины
|
||
- **THEN** ответ имеет код `201`: собственного порога по размеру у приёма нет
|
||
|
||
### Requirement: Опрос готовности задачи
|
||
|
||
Сервис SHALL отдавать рубеж аудиозаписи по запросу `GET /api/status/:id`
|
||
**только её владельцу**. Запрос без сессии MUST получать код `401`, и тело
|
||
такого ответа MUST не нести ни рубежа записи, ни текста расшифровки. Ответ
|
||
владельцу MUST нести идентификатор полем `job_id`, рубеж полем `status` и время
|
||
заведения полем `created_at`, а текст расшифровки полем `transcription_text`, и
|
||
это поле MUST отсутствовать в ответе, пока текста нет: пустая строка на месте
|
||
отсутствующего текста читается как «расшифровка пуста».
|
||
|
||
Видов текста у записи больше одного, поэтому ответ MUST называть вид, который
|
||
отдаёт: в поле `transcription_text` уходит **сырая расшифровка**, и только она.
|
||
Вычитанный текст этим полем MUST не подменяться — иначе значение поля менялось бы
|
||
у одной и той же записи от того, успел ли отработать необязательный шаг, а
|
||
контракт объявлен необратимым. Отдача «последнего записанного» текста MUST не
|
||
применяться: она делает ответ функцией порядка записи, а не состояния записи.
|
||
|
||
Перечень значений поля `status` MUST совпадать с перечнем рубежей конвейера:
|
||
`uploaded`, `normalized`, `submitted`, `transcribed`, `done`. Прежних значений
|
||
`created`, `converted`, `transcribe`, `failed` и `dead` в ответе MUST не быть.
|
||
Это объявленная ломка публичного контракта: рубеж называет достигнутое, а отказ
|
||
перестал быть состоянием.
|
||
|
||
Остановленная запись MUST отдавать рубеж, на котором она остановлена, и MUST
|
||
нести признак остановки отдельным полем `halted` со значением истины. Машинный
|
||
текст отказа MUST в ответ не попадать: он принадлежит журналу владельца сервиса,
|
||
а не отправителю. Этот адрес — **единственное** место, где отправитель узнаёт о
|
||
неудаче: доставки ответа отправителю у сервиса больше нет, и признак остановки
|
||
здесь несёт всю обязанность целиком.
|
||
|
||
Отказ без сессии MUST не зависеть от того, есть такая запись или нет: иначе по
|
||
кодам ответа перебирается список заведённых записей.
|
||
|
||
Запись, принадлежащая другому, MUST отвечать тем же, чем отвечает неизвестный
|
||
идентификатор, — кодом `404` и тем же телом.
|
||
|
||
#### Scenario: Запись найдена
|
||
|
||
- **GIVEN** отправитель предъявил сессию
|
||
- **WHEN** он спрашивает рубеж своей записи
|
||
- **THEN** ответ имеет код `200` и несёт `job_id`, `status` и `created_at`
|
||
- **AND** значение `status` принадлежит перечню рубежей конвейера
|
||
|
||
#### Scenario: Запись остановлена
|
||
|
||
- **GIVEN** запись остановлена признаком на рубеже приведения
|
||
- **WHEN** владелец спрашивает её рубеж
|
||
- **THEN** поле `status` несёт рубеж приведения
|
||
- **AND** поле `halted` несёт истину
|
||
- **AND** машинного текста отказа в ответе нет
|
||
|
||
#### Scenario: Сессии нет
|
||
|
||
- **WHEN** программа спрашивает рубеж заведённой записи без сессии
|
||
- **THEN** ответ имеет код `401`
|
||
- **AND** тело ответа не несёт ни рубежа записи, ни текста расшифровки
|
||
|
||
#### Scenario: Без сессии неизвестная запись неотличима от заведённой
|
||
|
||
- **WHEN** программа без сессии спрашивает рубеж заведённой записи, а затем
|
||
рубеж по неизвестному идентификатору
|
||
- **THEN** оба ответа имеют код `401`
|
||
|
||
#### Scenario: Чужая запись неотличима от неизвестной
|
||
|
||
- **GIVEN** запись заведена одним вошедшим
|
||
- **WHEN** её рубеж спрашивает другой вошедший
|
||
- **THEN** ответ имеет код `404` и то же тело, что и ответ по неизвестному
|
||
идентификатору
|
||
- **AND** тело ответа не несёт ни рубежа записи, ни текста расшифровки
|
||
|
||
#### Scenario: Расшифровки ещё нет
|
||
|
||
- **GIVEN** отправитель предъявил сессию
|
||
- **WHEN** он спрашивает рубеж своей записи, которая ещё не дошла до текста
|
||
- **THEN** поля `transcription_text` в ответе нет вовсе
|
||
|
||
#### Scenario: Записи с таким идентификатором нет
|
||
|
||
- **GIVEN** отправитель предъявил сессию
|
||
- **WHEN** программа спрашивает рубеж по неизвестному идентификатору
|
||
- **THEN** ответ имеет код `404` и сообщение о ненайденной записи
|
||
|
||
### Requirement: Имя файла, данное отправителем, не попадает в журнал
|
||
|
||
Приём SHALL не писать имя файла, данное отправителем, ни в одну свою журнальную
|
||
запись — ни на успешном пути, ни на пути отказа, где имя могло бы приехать
|
||
текстом ошибки. Имя приходит извне вместе с записью и принадлежит содержимому
|
||
личной переписки наравне с текстом расшифровки; журнал уезжает в собранные логи,
|
||
откуда строку не убрать.
|
||
|
||
Расширение, взятое из этого имени, в журнале остаётся собственным полем: по нему
|
||
прослеживается путь записи. Что именно попадает в журнал ради прослеживаемости,
|
||
нормирует требование ниже; наружу расширение выходит только приведённым к
|
||
известному виду — этому отдано отдельное требование.
|
||
|
||
Оговорка про второй вход из требования ушла вместе с ним: имя, данное
|
||
отправителем, доходит до сервиса единственным путём — приёмом по HTTP, — и
|
||
сценарии судят именно его.
|
||
|
||
#### Scenario: Имя записи не видно в журнале принятой записи
|
||
|
||
- **GIVEN** источник метаданных читает запись и отдаёт её длительность
|
||
- **WHEN** программа шлёт `POST /api/audio` с записью, чья основа имени несёт
|
||
опознаваемую строку при обычном расширении `.mp3`
|
||
- **THEN** ни одна журнальная запись приёма этой строки не содержит
|
||
- **AND** расширение `.mp3` в журнале допустимо
|
||
|
||
#### Scenario: Имя записи не видно в журнале при отказе приёма
|
||
|
||
- **GIVEN** источник метаданных не может прочитать запись
|
||
- **WHEN** программа шлёт `POST /api/audio` с записью, чья основа имени несёт
|
||
опознаваемую строку
|
||
- **THEN** ни одна журнальная запись приёма, включая запись об ошибке, этой
|
||
строки не содержит
|
||
|
||
### Requirement: Поднятые входы видны наблюдателю
|
||
|
||
Сервис SHALL отдавать признак поднятости по каждому входу приёма отдельной
|
||
метрикой и MUST выставлять метку только тому входу, который у сервиса есть.
|
||
Метки убранного входа в метриках MUST не быть вовсе: признак со значением нуля
|
||
читался бы как «вход есть, но не поднялся», то есть как поломка, а вечная
|
||
единица рядом с ним — как исправность того, чего нет.
|
||
|
||
Проверяемое здесь одно — **набор меток**, и это честнее прежнего. Вход остался
|
||
один, страница метрик отдаётся тем же сервером, что и приём, и значение нуля у
|
||
единственной метки недостижимо: чтобы прочитать признак, надо дотянуться до
|
||
входа, о котором он сообщает. Прежнее обоснование — «иначе потерянный вход не
|
||
виден ничем» — было верно, пока входов было два; сегодня неподнятый вход виден
|
||
неудачей чтения самих метрик.
|
||
|
||
Различать поднятый и неподнятый вход признак MUST снова, как только входов у
|
||
сервиса станет больше одного.
|
||
|
||
#### Scenario: В метриках только оставшийся вход
|
||
|
||
- **GIVEN** сервис поднялся
|
||
- **WHEN** наблюдатель читает метрики
|
||
- **THEN** признак поднятости несёт метку входа HTTP со значением единицы
|
||
- **AND** метки убранного входа Telegram в метриках нет вовсе
|
||
|
||
## REMOVED Requirements
|
||
|
||
### Requirement: Признак включения решает, поднимается ли вход Telegram
|
||
|
||
**Reason**: Вход Telegram убран из сервиса целиком, и решать о его подъёме стало
|
||
нечего. Настройки входа — признак включения, ключ доступа и срок ожидания
|
||
обновлений — уходят из конфигурации вместе с ним.
|
||
|
||
**Migration**: Секцию `[telegram]` и ключ `server.users_while_list` — имя в коде
|
||
именно такое, с опечаткой, и в боевом файле искать надо его — из файла настроек
|
||
убрать руками. Оставленные ключи сервис пропускает молча — незнакомые
|
||
ключи разбор настроек не судит, — и потому файл, забытый как есть, поднимет
|
||
сервис без бота и без единого слова о том, что секция больше ничего не значит.
|
||
Записи с источником Telegram, если они в базе есть, остаются нетронутыми и
|
||
достаются своему владельцу; ответ в чат по ним не уходит. Возврат входа заводится
|
||
новым изменением вместе со связью чата и учётной записи.
|