Формат основан на Keep a Changelog, проект следует Semantic Versioning.
- [1.5.1] -- 2026-07-30
- [1.5.0] -- 2026-07-30
- [1.4.0] -- 2026-05-29
- [1.3.0] -- 2026-05-27
- [1.2.7] -- 2026-05-18
- [1.2.6] -- 2026-05-18
- [1.2.5] -- 2026-05-08
- [1.2.4] -- 2026-05-07
- [1.2.3] -- 2026-05-07
- [1.2.2] -- 2026-05-07
- [1.2.1] -- 2026-05-07
- [1.2.0] -- 2026-05-07
- [1.1.0] -- 2026-05-02
- [1.0.0] -- 2026-03-28
- Классификация чатов, из которых аккаунт удалён или заблокирован (
ChatForbidden,ChannelForbidden). Ранее такой чат попадал в типpersonalс предупреждениемUnknown entity class; теперь он определяется какprivate_group,private_supergroupилиprivate_channelи помечается признакомis_left, поэтому попадает в разделleftкаталога и пропускается при экспорте по умолчанию (left_channels.action = skip) -- вместо гарантированной ошибки запроса недоступной истории.
- Опции
--truncate Nи--no-truncateдля командыtg messages: длина обрезки текста сообщения задаётся явно,--truncate 0и--no-truncateпечатают текст целиком. По умолчанию сохранено прежнее поведение -- обрезка на 200 символов. Одновременное указание--no-truncateс ненулевым--truncate-- ошибка разбора аргументов (код2).
Выпуск устраняет 42 находки автоматического ревью project-check.
- Глобальные флаги
--quiet/-q(подавление прогресса и статусов; ошибки и итоговая сводка сохраняются) и--log-level(уровень логирования с поддержкой переменной окруженияLOG_LEVEL; приоритет--debug>--log-level>LOG_LEVEL>WARNING). - Флаг
--jsonдля машиночитаемого вывода командaccount list,auth check,state show. - Базовый класс доменных ошибок
TgExportError(tg_export/errors.py); все доменные исключения наследуются от него. - Общий модуль форматирования tg_export/format.py (
format_size). - Файлы
.editorconfig,RELEASING.md, каталогdocs/adr/; разделы README про глобальные опции и автодополнение.
- Диагностика, прогресс и логи направлены в stderr; stdout оставлен только для машиночитаемого вывода команд
list,state show,tg info,tg messages— безопасный пайпинг (tg-export list --format json | jq ...). - Прерывание команды
runсигналом завершает процесс с кодом130(SIGINT) или143(SIGTERM) вместо0. - Доменные ошибки на верхнем уровне выводятся кратким сообщением и кодом выхода
1; полный traceback — только под--debug(обёрткаrun_cli). - Точка входа консольного скрипта изменена на
tg_export.cli:run_cli. - Порог покрытия тестами
cov-fail-under=45;codecov-actionобновлён до v6.0.1; перед публикацией на PyPI прогоняются тесты и smoke-тест собранного wheel. - Все комментарии в исходном коде переведены на английский.
_export_userpics: имя файла больше не переиспользуется при неуспешной загрузке (отдельный счётчик на каждой итерации).- Конфликт
offset_id/offset_dateв фазе 2 при заданномdate_to; атомарный commit прогресса фазы 2. - Проглатывание ошибок (скачивание фото профиля) заменено логированием; в warning'ах экспорта добавлен stack trace (
exc_info). - Ссылка на чат в индексе учитывает
sanitize_name(folder). - Потокобезопасный доступ к
active_downloadsмежду event loop и потоком обновления Rich Live; ограничен рост словаря блокировокfile_id; jitter в retry скачивания. - Валидация значений
action/formatи неизвестных ключей YAML-конфига при загрузке. purge_chatвыполняет удаление в одной транзакции; запросы по диапазону дат используют индекс.
- Флаг
--version(tg-export --version): выводит версию пакета из метаданных установленного дистрибутива (importlib.metadata), номер версии в коде не дублируется.
- Fail-fast при заданном в конфиге
proxy, но отсутствующем пакетеpython-socks: ранее Telethon молча игнорировал прокси и подключался напрямую, раскрывая реальный IP. ТеперьTgApiпрерывает запуск с понятной ошибкой и подсказкой по установке extra[proxy](tg_export/api.py).
- GitHub Actions: автоматическое создание GitHub Release при пуше тега
v*(.github/workflows/publish.yml): вырезает секцию изCHANGELOG.mdи создаёт release черезgh release create. - CI: шаг
uv lock --checkв.github/workflows/ci.ymlпроверяет синхронизациюuv.lockсpyproject.toml. Расхождение версий теперь валит CI и не попадёт в следующий релиз. - CLAUDE.md: раздел «Релиз и версионирование» -- при бампе версии / изменении зависимостей обязателен
uv lock.
_extract_and_clearвFixedSQLiteSession: type-валидацияtakeout_idиtmp_auth_key,has_dataчерезis not None(раньшеbool(b'')==Falseпропускал пустые BLOB как «нет данных»). Telethon приstore_tmp_auth_key_on_disk=Falseпишетb''в physical позициюtmp_auth_key, а swap-баг при следующем чтении ставитsession._takeout_id = b'', что валит сериализаторInvokeWithTakeoutRequest(takeout_id=b'')сstruct.error: required argument is not an integer. Дополнительно post-init валидация: если послеsuper().__init__()_takeout_idоказался non-int -- clear.- Атомарный
commit_phase_progressвExportState(tg_export/state.py). Ранее фаза 2 делала 4 раздельных commit-а (last/oldest/full_history/messages_count); прерывание сети между ними оставляло несогласованное состояние, иset_oldest_msg_idмог упасть сNOT NULL constraint failed: export_state.last_msg_id. Все четыре сеттера переведены на единый_upsert_chat_stateс whitelist колонок и явнымlast_msg_id=0в INSERT-ветке. Схема:last_msg_id INTEGER NOT NULL DEFAULT 0. - Кооперативное прерывание HTML-рендера в
render_chat_streamingи_render_indexчерез параметрshould_stop. Force shutdown зависал после «state saved», потому чтоasyncio.to_threadставит задачу в defaultThreadPoolExecutor,task.cancel()прерывает толькоawait, а сам thread продолжает Jinja2-рендер;asyncio.run().__exit__ждёт thread join() с таймаутом. Гранулярность прерывания -- month bucket; внутри одногоjinja2.render()прерывания нет (CPython не позволяет прервать thread без yield). chat_error_lineпринимает опциональныйchat_id, и сообщение об ошибке экспорта чата включает id (Error exporting Alice (id=12345): ...) для возможности ручной правки записи вexport_state.
cast(Any, ...)для возвращаемых значений Telethonget_personal_info()/get_top_peers(): Pyright не разрешал доступ к.full_user/.users/.categoriesдляUnionбез stubs.
- Замена workaround'а 1.2.4 на корректный фикс через subclass
FixedSQLiteSession(tg_export/session.py). Передsuper().__init__()читаемtakeout_id/tmp_auth_keyявно по именам колонок, обнуляем их на диске (чтобы баггованный позиционный unpack в Telethon не крашился наAuthKey(data=int)), затем восстанавливаем значения через сеттеры -- write-путь у Telethon корректный. Главное преимущество:takeout_idтеперь сохраняется между запусками, прежний sanitize терял его и заставлял каждый запуск создавать свежий takeout (cooldown TAKEOUT_INIT_DELAY и повторное подтверждение в клиенте Telegram). Регрессионный тест вtests/test_api.pyявно проверяет, что ванильныйSQLiteSessionкрашится на той же фикстуре, на которой наш subclass работает корректно.
- Workaround для асимметричного бага Telethon
SQLiteSession1.43+ (commit 5a3a94eb):_update_session_tableпишет в порядке(..., auth_key, takeout_id, tmp_auth_key), а__init__читаетselect *и распаковывает как(..., key, tmp_key, takeout_id)-- 5-й и 6-й столбцы переставлены местами. Пока обе колонкиNULL,AuthKey(data=None)срабатывает по early-return и баг не виден; как только Takeout-экспорт сохраняетtakeout_id, при следующем старте этот int попадает вtmp_key, и Telethon крашится сTypeError: object supporting the buffer API requiredизsha1(int). Перестановка колонок не помогает -- read/write симметрично сломаны.TgApi.__init__теперь обнуляетtmp_auth_keyиtakeout_idчерез_sanitize_session_fileперед открытиемTelegramClient.auth_key(256 байт) сохраняется, перелогин не нужен; нашstart_takeoutвсё равно начинает свежий takeout. Правильный фикс через monkey-patchSQLiteSessionзаписан в TODO.md.
start_takeoutавтоматически завершает (или локально обнуляет) стейлsession.takeout_idперед началом новой Takeout-сессии. Раньше Telethon бросалValueError("Can't send a takeout request while another takeout for the current session still not been finished yet.")изTakeoutClient.__aenter__без обращения к серверу, и пользователю приходилось вручную запускатьtg-export takeout clear.
[proxy]extras: вернулиpython-socks[asyncio]>=2.0. Telethon 1.43+ проверяетpython_socksи при его отсутствии молча игнорирует аргументproxy=с warning'ом "proxy argument will be ignored because python-socks is not installed". PySocks-путь вconnection.pyгейтится той же проверкой и не запускается; в 1.2.1 прокси из конфига фактически не применялся.
[proxy]extras: ошибочный переход наPySocks>=1.7. Откачено в 1.2.2.
- Включено
autoescapeв Jinja2-шаблонах HTML-рендера: имена чатов, авторов, контактов, sessions и stories больше не позволяют XSS через<script>или<img onerror>. - Добавлен whitelist URL-схем (
http,https,mailto,tel,tg) иrel="noopener noreferrer"для всехtarget="_blank"-ссылок:javascript:/data:-инъекции через Telegram URL-entities и inline-кнопки больше не приводят к исполнению JS. sanitize_nameотбрасывает.., управляющие символы, RTL-override; нормализует Unicode (NFKC); ограничивает длину 200 байт.purgeбольше не используетrglob-- сканирует только известные префиксыunfiled/,archived/,left/,folders/*и проверяет, что путь действительно внутриoutput_base.resolve(). Симлинки пропускаются.tdesktopimport: пути из внешнего HTML валидируются черезis_relative_to(chat_dir.resolve()).- Sibling-БД: путь к файлу проверяется на принадлежность tree соседа; размер сравнивается с заявленным Telegram.
- Конфигурация:
~/.config/tg-exportпринудительно получает 0o700;api_credentials.yamlвалидируется на типы и слабые права.
- Логика
last_msg_idв фазе 2: накапливаем максимум черезphase2_max_id, а не теряем после первого сообщения. register_fileтеперь явно делаетcommit: при kill -9 файл не остаётся незарегистрированным в БД._verify_filesиспользует тот же путьregister_file -> commit, поэтому verify-результаты сохраняются и не теряются приclose()._handle_shutdownчерезasyncio.shieldзащищает идущийcommitот отмены: повторный SIGINT не теряет batch до 500 сообщений._cleanup_orphaned_filesсравнивает абсолютные resolved-пути; запуск из другой cwd больше не приводит к удалению легитимных файлов как orphaned._download_if_newсравнивает SHA-256 первых 64KB, а не только размер: два разных файла одной длины больше не схлопываются в один.start_takeoutотлавливаетTakeoutInvalidError/TakeoutRequiredErrorнапрямую, а не через подстроку вstr(e).cache_catalogявно коммитит запись о чате: статистика не теряется, если экспорт прервался до первого batch-commit.- Сообщение об отсутствии default-аккаунта указывает правильную команду
tg-export account default, а не несуществующуюauth default.
rich>=15.0: обновлено с 14.x. API Live/Progress/Console сохранён.click>=8,pytest>=8,telethon>=1.36,pygmentsобновлены до актуальных версий.- В dev добавлены
pytest-cov,pytest-timeout,ruff,pyright.
ruff(lint + format) иpyright(basic) включены в CI как блокирующие шаги. На текущий момент 0 ошибок и 0 предупреждений.- pytest:
--strict-markers --strict-config -ra,timeout=60,asyncio_mode=auto, фильтрerror::DeprecationWarning. - Тесты на XSS, URL-схемы, lock-файл, sanitize_name, credentials.
- SQLite PRAGMA:
journal_mode=WAL,synchronous=NORMAL,cache_size=-65536,mmap_size=268435456,temp_store=MEMORY,foreign_keys=ON. - Lock-файл
<state>.db.lockчерезfcntl.flock: защита от случайного второго процесса экспорта над одной БД. - Стриминговый рендер по месяцам через
render_chat_streaming: пиковая память пропорциональна одному месяцу, а не всему чату. - HTML-рендер вынесен в
asyncio.to_thread: больше не блокирует event loop на крупных чатах. - Per-
file_idasyncio.LockвMediaDownloader: параллельные сообщения с одинаковымfile_idсериализуются и получают cross-chat dedup. - Индексы SQLite:
idx_files_chat,idx_files_status,idx_files_local_path,idx_messages_grouped. state.list_message_months,state.load_messages_for_month,state.get_catalog_entry.tg sendтеперь предупреждает о best-effort семантике и выводитN/Mпосле неуспешных получателей._register_skip-- skipped_by_size/skipped_by_type записываются в БД, чтобы verify/count корректно их различали.
- GitHub Actions запинены на полный commit SHA (
actions/checkout,astral-sh/setup-uv,pypa/gh-action-pypi-publish); версии обновлены до актуальных. permissions: contents: readна уровне workflow вci.yml;publish.ymlдополнительно ограничен необходимым.timeout-minutesдля всех jobs (15 для test, 10 для publish).enable-cache: trueдля setup-uv сcache-dependency-glob: uv.lock.
- Эскейп rich-markup в именах чатов и файлов; устранена гонка в
Live-выводе.
- Первый стабильный релиз: инкрементальный экспорт, sibling-дедупликация, импорт из tdesktop, HTML-рендер по месяцам.