удалён вход Telegram, владелец записи стал обязателен в схеме
- убраны клиент бота, транспорт обновлений, отправитель сообщений, сборка входа при старте, секция настроек и зависимость go-telegram-bot-api; из конвейера ушла доставка ответа отправителю — исход виден опросом готовности. Колонки адресата и значение источника остались в схеме: применённые шаги не переписываются - шаг 202608140003 запрещает пустого владельца у аудиозаписи и у файла; существующие строки он не проверяет, и это принято сознательно — искать их надо запросом до выкладки - ревью нашло два пред-существующих дефекта, оба закрыты: пустой второй ответ распознавателя стирал сохранённую расшифровку, а пустая расшифровка перестала быть заметной вместе с убранной доставкой. Попутно поднят golang.org/x/image до v0.45.0 — красный шаг vulns, воспроизводился и на чистом master
This commit is contained in:
@@ -0,0 +1,225 @@
|
||||
## Context
|
||||
|
||||
Сервис принимает записи двумя входами. Приложение и внешняя программа приходят по
|
||||
HTTP с сессией, и у их записей есть владелец; бот приходит от чата, и у его
|
||||
записей владельца нет — связи чата с учётной записью сервис не ведёт. Отсюда
|
||||
оговорка про бота в каждом решении о владельце: в спеке приёма, в спеке доступа,
|
||||
в схеме хранилища и в отборе записей.
|
||||
|
||||
Вход Telegram при этом несёт собственный вес: клиент, транспорт обновлений,
|
||||
разбиение длинного текста на сообщения, заглушка отправителя, сборка входа при
|
||||
старте с разбором «недоступность или ошибка настройки», список допущенных людей,
|
||||
доставка ответа из конвейера и правило о недоставленном ответе.
|
||||
|
||||
Ограничение, которое правит решением: применённый шаг схемы не переписывается —
|
||||
это инвариант проекта, и он **critical**. Колонки `tg_chat_id`,
|
||||
`tg_reply_message_id` и значение `telegram` перечня `source` заведены шагами
|
||||
`202608110001` и `202608140002`, оба применены. В боевой базе по этим колонкам
|
||||
лежат записи живых людей.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Убрать вход Telegram из кода, настроек и зависимостей целиком.
|
||||
- Сделать владельца записи безусловным: завести запись без владельца сервису
|
||||
нечем.
|
||||
- Сделать владельца обязательным **в схеме**, а не только в приёме: ни одной
|
||||
записи не потерять и ни одного применённого шага схемы не переписать.
|
||||
- Оставить возврат входа дешёвым: удалённое лежит в истории git одним изменением,
|
||||
и возвращается оно вместе со связью чата и учётной записи.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- Замена доставки ответа. Уведомлений взамен чата не заводим — на это стоит
|
||||
задача `ntfy-delivery`.
|
||||
- Чистка хранилища от колонок и значений бота. Ни нового шага схемы, ни правки
|
||||
применённых.
|
||||
- Переделка приёма по HTTP, конвейера, распознавания и панели.
|
||||
- Связь чата Telegram с учётной записью. Она нужна возврату входа, и заводит её
|
||||
своя задача.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Схема теряет только необязательность владельца
|
||||
|
||||
Колонки чата и ответного сообщения остаются, значение `telegram` в перечне
|
||||
источников остаётся, применённые шаги не переписываются. Новый шаг схемы у
|
||||
изменения один, и делает он ровно одно: колонка владельца у аудиозаписи и у файла
|
||||
перестаёт принимать пустое значение.
|
||||
|
||||
Шаг безопасен потому, что записей без владельца в боевой базе нет — это сказал
|
||||
владелец сервиса, отвечая на прямой вопрос, и на этом ответе решение и стоит.
|
||||
Будь такие записи, шаг пришлось бы либо отменить, либо оплатить необратимой
|
||||
правкой чужих данных: назначить им владельца выдумкой или удалить.
|
||||
|
||||
**Проверяется это запросом, а не прогоном шага**, и разница выяснилась ревью с
|
||||
оракулом: хранилище держит обязательность связи проверкой записи при сохранении,
|
||||
а не ограничением таблицы. Смена признака на базе с ничьей записью проходит
|
||||
зелёным и такую запись оставляет — то есть прогон шага на копии боевой базы
|
||||
чистую базу от грязной не отличает. Заставить шаг считать строки самому владелец
|
||||
решил не делать (решение от 2026-08-14): безопасность держится ручной проверкой,
|
||||
и она названа первым шагом плана перехода.
|
||||
|
||||
Цена промаха, если ничью запись всё же проглядят: она становится незакрываемой.
|
||||
Захват идёт сырым запросом мимо проверки и выдаёт её воркеру, а всякое сохранение
|
||||
отказывает — включая то, которым ставится признак остановки. Запись повторяется
|
||||
неограниченно, не оставляя следа ни в метрике, ни в журнале событий.
|
||||
|
||||
Колонка темы словаря обязательной была уже — разное правило у трёх колонок одного
|
||||
смысла читалось бы как недосмотр, и теперь их правило одно.
|
||||
|
||||
Рассмотрено и отвергнуто:
|
||||
|
||||
- **Новый шаг, убирающий колонки бота.** Он законен — запрет стоит на правке
|
||||
применённого шага, а не на новом, — но необратим по данным: колонки заполнены у
|
||||
записей живых людей, и восстановить их после удаления неоткуда. Возврат входа
|
||||
завёл бы их заново пустыми.
|
||||
- **Сужение перечня `source` до `unknown` и `api`.** Строки со значением
|
||||
`telegram` в базе есть, и после сужения перечня они перестают проходить
|
||||
проверку схемы: панель откажется их сохранять, а поведение записи при чтении
|
||||
зависит от библиотеки. Цена — молчаливая порча уже принятых записей.
|
||||
|
||||
Поля адресата уходят при этом из **модели** — колонки остаются в схеме, а
|
||||
аудиозапись их больше не несёт. Это безопасно ровно потому, что колонки адресата
|
||||
пишет только заведение записи: снимок шага конвейера кладётся отображением, где
|
||||
их нет вовсе, и потому значение уже принятой записи он не затирает. Правила
|
||||
`internal/archrules` такую пару держат: колонка, которую никто не пишет, законна,
|
||||
незаконна колонка, которую пишут, но не читают.
|
||||
|
||||
Следствие: константа `entity.SourceTelegram` остаётся в модели. На неё ссылается
|
||||
применённый шаг `202608140002`, и убрать её значило бы переписать применённый
|
||||
шаг. В модели она получает комментарий об историческом значении: новых записей с
|
||||
ним не появляется.
|
||||
|
||||
### Владелец записи перестаёт быть необязательным и в модели
|
||||
|
||||
Поле владельца в аудиозаписи становится обычной строкой вместо ссылки, которой
|
||||
позволено отсутствовать. Модель здесь повторяет схему, а не расходится с ней:
|
||||
значения «владельца нет» больше не существует ни на одном уровне.
|
||||
|
||||
Рассмотрено и отвергнуто:
|
||||
|
||||
- **Оставить ссылку, которой позволено отсутствовать.** Она заставляет каждого
|
||||
читателя решать, что делать с отсутствием, хотя отсутствия больше не бывает.
|
||||
Два способа выразить одно значение расходятся молча.
|
||||
- **Держать обязательность одним приёмом, схему не трогать.** Так было задумано
|
||||
сперва, и это оставляло дыру: ничью запись заводили руками в панели, она
|
||||
уходила в конвейер, стоила денег на распознавание и не доставалась потом
|
||||
никому. Решение владельца от 2026-08-14 — обязательность держит схема.
|
||||
|
||||
Правило «пустой владелец не совпадает ни с одной записью» при этом остаётся и не
|
||||
становится избыточным: схема запрещает **заводить** ничью запись, а это правило
|
||||
запрещает **спрашивать** ничьим именем. Снять его, сославшись на схему, значит
|
||||
открыть выборку первому же вызывающему без учётной записи.
|
||||
|
||||
### Отправителя о готовности и об остановке узнаёт опрос, и другого канала нет
|
||||
|
||||
Доставка из конвейера убирается вместе с договором об отправителе сообщений.
|
||||
Правило о недоставленном ответе уходит: недоставке взяться неоткуда.
|
||||
|
||||
Рассмотрено и отвергнуто:
|
||||
|
||||
- **Оставить заглушку отправителя.** Договор без единой реализации, кроме
|
||||
пустой, — это мёртвый шов, который читается как незаконченная работа и
|
||||
переживает не одну задачу.
|
||||
- **Завести уведомление взамен сразу.** Это другой предмет со своей внешней
|
||||
зависимостью; задача на него уже стоит в беклоге, и слепить их значит собрать
|
||||
два изменения в одно.
|
||||
|
||||
### Метка убранного входа не выставляется вовсе
|
||||
|
||||
Признак поднятого входа остаётся, метка `telegram` у него больше не появляется.
|
||||
Ноль вместо неё читается как «вход есть, но не поднялся», то есть как поломка;
|
||||
владелец, у которого на этот признак стоит отбор, увидел бы аварию на ровном
|
||||
месте.
|
||||
|
||||
### Незнакомые ключи настроек по-прежнему не судятся
|
||||
|
||||
Секция `[telegram]` и ключ `server.users_while_list` убираются из структуры
|
||||
настроек и из образца. Имя ключа написано с опечаткой — `while` вместо `white`, —
|
||||
и это записано «Расхождением» в `docs/conventions/config.md`; удаление ключа
|
||||
закрывает и его. Файл, где их забыли, сервис поднимет молча: разбор настроек
|
||||
незнакомые ключи не проверяет.
|
||||
|
||||
Рассмотрено и отвергнуто: **завести отказ старта по незнакомому ключу**. Это
|
||||
новое поведение настроек, полезное само по себе, но чужое этой задаче: оно
|
||||
касается всех ключей, а не убранных, и разбирается своей задачей.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **У сервиса не остаётся входа, которым человек может воспользоваться.** Это
|
||||
главный риск изменения, и он не смягчается ничем внутри задачи. Приложения нет,
|
||||
своего токена у программы нет, значит после выкладки положить запись можно
|
||||
единственным способом: собранным руками запросом с сессией, снятой из браузера
|
||||
после входа. Срок этого состояния задаётся чужими задачами — экраном приложения
|
||||
и личными токенами, — и внутри этой задачи не назначается. Прецедент в журнале
|
||||
ревью: 2026-08-12 поверхность закрыли так, что войти не мог никто, и спасением
|
||||
тогда был именно бот.
|
||||
- **Боевой файл настроек переживёт выкладку с мёртвой секцией** → сервис
|
||||
поднимется без бота, и это ровно то, чего мы хотим; секцию убирает человек при
|
||||
выкладке, и об этом сказано в плане перехода.
|
||||
- **Записи, застрявшие в конвейере на минуту выкладки, дойдут до текста, и ответа
|
||||
в чат по ним не уйдёт** → отправитель в Telegram ответа не получит вовсе.
|
||||
Смягчения нет и быть не может: чат — это и есть убираемый вход. Расшифровка не
|
||||
теряется, владелец сервиса видит её в панели.
|
||||
- **Ничья запись, если она всё же найдётся, шагом не ловится** → она остаётся в
|
||||
базе и становится незакрываемой: сохранить её нечем, остановить тоже, а следа
|
||||
не остаётся ни в метрике, ни в журнале событий. Смягчение одно и ручное —
|
||||
проверка запросом первым шагом плана перехода. Заставить шаг считать строки
|
||||
самому владелец решил не делать.
|
||||
- **Возврат входа стоит восстановления кода** → удаление уезжает одним
|
||||
изменением, и его архив вместе с историей git даёт точку возврата. Связь чата с
|
||||
учётной записью всё равно потребует нового кода: без неё возвращать вход значит
|
||||
возвращать записи без владельца.
|
||||
- **Правила, потерявшие предмет, останутся в памятке и в модели угроз** →
|
||||
инварианты про белый список бота и про ответ отправителю убираются синком
|
||||
документации тем же изменением, а не следующей задачей.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. Проверить боевую базу запросом: `SELECT count(*) FROM audio_records WHERE
|
||||
owner = ''` и то же по `files`. Оба должны отдать ноль. Прогон самого шага на
|
||||
копии этого не показывает — он проходит зелёным и на грязной базе.
|
||||
2. Собрать образ (`task image`) — сборка образа в гейт не входит, и человек
|
||||
обязан прогнать её сам.
|
||||
3. Выложить (`inv pl -- transcriber` из `pet-project-server`) — запускает человек.
|
||||
4. Убрать из боевого файла настроек секцию `[telegram]` и ключ
|
||||
`server.users_while_list`. Порядок с шагом 3 безразличен: оставленные ключи
|
||||
сервис пропускает молча, и для подъёма этот шаг не нужен.
|
||||
5. Ключ доступа к боту остаётся в хранилище выкладки под возврат входа — решение
|
||||
владельца от 2026-08-14. Ротация не делается, бот у Telegram остаётся
|
||||
зарегистрированным. Цена решения названа прямо: отправитель голосового не
|
||||
получит ни ответа, ни отказа и не отличит выведенный вход от сломанного
|
||||
сервиса. Оповещать людей из списка допущенных владелец не стал.
|
||||
6. Откат — прежний образ. Он потребует вернуть настройки: сервис прошлой версии
|
||||
без ключа `telegram.enabled` не поднимается. Схему откат вернёт своим
|
||||
обратным шагом — колонка владельца снова примет пустое значение, — и на
|
||||
записях это не сказывается: пустых среди них нет.
|
||||
|
||||
### Формы решения, между которыми выбирали
|
||||
|
||||
Форма выбрана не единственной рассмотренной, и остальные две названы здесь, а не
|
||||
подразумеваются:
|
||||
|
||||
- **Выключить вход признаком, код оставить.** Признак `telegram.enabled` заведён
|
||||
2026-08-13 и обязателен, а приём по HTTP владельца уже требует: одна правка
|
||||
ключа в боевом файле даёт «новых записей без владельца не заводится» ценой ноля
|
||||
строк кода и мгновенным возвратом. Отвергнуто по причине из раздела «Why»:
|
||||
двойная модель остаётся в коде, и оговорку про бота продолжает платить каждая
|
||||
следующая задача. Размен здесь честный — стоимость сопровождения против
|
||||
стоимости отката, — и выбран он в пользу первой.
|
||||
- **Сузить бота до исходящего канала.** Приём убрать, отправку оставить с одним
|
||||
адресатом — чатом владельца строкой настроек; связь чата с учётной записью для
|
||||
этого не нужна. Смягчает главный риск: человек хотя бы узнаёт исход. Отвергнуто
|
||||
потому, что заводит понятие «канал уведомления владельца», которое тут же
|
||||
переделает задача `ntfy-delivery`, и держит ради этого клиент, договор и
|
||||
зависимость — плату за временный мост.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Критерии приёмки постановка не назвала: они предложены в `tasks.md` и
|
||||
подтверждаются человеком на чекпоинте.
|
||||
- Судьба задач беклога `telegram-account-link` и
|
||||
`bot-api-only-through-bot-client`: обе теряют предмет до возвращения входа.
|
||||
Решает владелец; это изменение их не трогает.
|
||||
Reference in New Issue
Block a user