Skip to content

v1.2.0

Choose a tag to compare

@geekcodev geekcodev released this 01 Oct 11:15
· 6 commits to main since this release
183bacd

v1.2.0 — Реестр чатов: одна строка на чат

Релиз адаптера geekcodev/laravel-max-client поверх ядра geekcodev/max-php-client: ^1.1.8. Предыдущий тег — v1.1.4.
Версия MINOR, ломающих изменений два: они описаны в разделе «Изменение (BC)».

Новое

  • Таблица связей max_chat_users: модель Models\MaxChatUser, настройка chats.chat_users_model
    (MAX_CHAT_USERS_MODEL). Это реестр зафиксированных взаимодействий пользователя с ботом, а не состав группы или
    канала: строки появляются только из апдейтов, в которых пришёл конкретный user_id.
  • Связи моделей: MaxChat::maxUsers() и MaxChat::chatUsers(), MaxUser::chatLinks().
    MaxUser::maxChats() сохранена и теперь проходит через max_chat_users (внутри hasManyThrough).
  • Аддитивная миграция 0001_01_01_000003_create_max_chat_users_table — новая таблица.
  • Команда php artisan max:upgrade — перенос данных существующих установок: пары «чат + пользователь» уезжают
    в max_chat_users вместе со статусом и last_activity_at, дубли чата сливаются в одну строку, после чего user_id
    и суррогатный id удаляются, а chat_id становится первичным ключом. Опции: --dry-run (план без применения),
    --force (без подтверждения), --rollback (вернуть прежнюю форму). Логика — Services\MaxSchemaUpgrade,
    команда идемпотентна.
  • Схема реестров: у max_chats первичный ключ — сам chat_id из MAX (строка одна на чат, суррогатного id нет),
    у max_users — user_id. Единственный суррогатный ключ — id строки связи max_chat_users: uuid (uuid7, значение
    выдаёт модель через HasUuids). Отдельный индекс на max_chat_users.chat_id не добавляется: он полностью покрыт
    левым префиксом UNIQUE (chat_id, user_id).
  • Индексы на max_chats.status и max_chats.last_activity_at — по ним работают activeChatIds() и
    max:chats:refresh. Установленным проектам их добавляет max:upgrade, чтобы схема после обновления совпадала
    со схемой чистой установки.

Изменение (BC)

  • max_chats хранит одну строку на чат. Прежде в ней лежала строка на пару «пользователь + чат» с уникальным ключом
    (user_id, chat_id). Колонки user_id и суррогатный id удалены, первичным ключом стал chat_id.
  • Колонка max_chats.title_checked_at переименована в chat_checked_at. Отметка ставится один раз на проверку
    getChat и относится ко всем метаданным сразу (название, описание, ссылка, иконка), а не к одному названию.
    Переименование выполняет max:upgrade с сохранением значения времени (в --rollback прежнее имя возвращается,
    иначе код v1.1.4 упал бы на отсутствующей колонке). Установка v1.1.3 и старше аддитивных миграций метаданных не
    получала: пересборка читает только существующие колонки, поэтому обновление проходит, а чат попадает в очередь
    max:chats:refresh один раз.
  • Настройка интервала перепроверки переименована: chats.title_check_interval → chats.chat_check_interval,
    MAX_CHATS_TITLE_CHECK_INTERVAL → MAX_CHATS_CHAT_CHECK_INTERVAL. Прежние имена остаются рабочими: если нового
    ключа в конфиге нет вовсе (конфиг опубликован до v1.2.0), читается старый; если новая переменная окружения пуста
    (так заполнен .env.example), читается прежнее имя переменной. Явно выставленный новый ключ — в том числе 0 —
    всегда побеждает прежний, иначе переопределить старый интервал было бы нечем.
  • Суррогатный id строки max_chats удалён, а chat_id стал первичным ключом. Смена первичного ключа
    выполняется пересборкой таблицы (временная таблица в целевой форме, копирование одним insert … select,
    переименование): на месте ключ сменить нельзя — SQLite отклоняет удаление колонки ключа («cannot drop PRIMARY KEY
    column») и молча игнорирует primary() вне create, MySQL требует отдельных команд. MaxChat больше не использует
    HasUuids: $primaryKey = 'chat_id', $incrementing = false, $keyType = 'int'. У max_chat_users суррогатный
    uuid-ключ остаётся — идентификаторов от MAX у строки связи нет, поэтому вставка идёт через Eloquent: голый
    DB::table('max_chat_users')->insert() без id не сработает, у колонки нет значения по умолчанию на стороне базы.
  • MaxChat::maxUser() удалён — метод был привязан к прежней форме. Замена: $chat->maxUsers() (HasManyThrough) или
    $chat->chatUsers()->first()?->maxUser для первого пользователя.
  • max:upgrade вместо реструктурирующей миграции. Перенос данных делает команда, а не файл в таблице
    migrations: перед изменением схемы виден план (--dry-run), результат откатывается (--rollback), а у потребителя
    не появляется лишняя запись в migrations. Обновление стало двухшаговым: php artisan migrate (создаёт таблицу
    связей) и php artisan max:upgrade (переносит данные). Ручные правки схемы команда, как и прежняя её версия,
    не закрывает.
  • MaxChatProfileService::inheritKnownMetadata() удалён: с одной строкой на чат наследовать нечего.
  • Create-миграции 000001/000002 переписаны под новую схему. Для новых установок это обычные миграции; у
    установленных проектов имена 000001/000002 числятся выполненными, поэтому их форму меняет миграция 000004.
    Их форму меняет max:upgrade. У потребителей, которые копировали миграции вручную (см. README, раздел «Если миграции
    пакета публиковались раньше»), копии обязательно удаляются, иначе они перекроют файлы пакета.
  • Апдейт без объекта user больше не пишет max_users, но строка чата заводится и обновляется, а связь
    max_chat_users — только при известном user_id.

Исправления

  • bot_removed без user больше не теряется и для неизвестного чата. Прежде новая строка не заводилась, потому что
    user_id был частью уникального ключа; чат навсегда оставался active, activeChatIds() его возвращал, а
    max:chats:refresh бесконечно опрашивал getChat. Теперь строка чата создаётся по одному chat_id.
  • Метаданные чата запрашиваются один раз на чат. Проверка «есть ли заполненная строка» больше не зависит от числа
    участников: 50 участников группы дают один getChat, а не пятьдесят.
  • MaxChat::displayName() при пустом title берёт имя первого известного пользователя чата.
  • Колонки идентификаторов объявлены знаковым BIGINT. Прежде chat_id и user_id были unsigned, а
    идентификаторы групп и каналов в MAX отрицательные (владелец прислал реальный -79032376695376): на MySQL такой
    чат в реестр не попадал (Out of range value), причём SQLite знаковость игнорирует, поэтому тесты баг не видели.
    Знаковым объявлены и user_id — идентификаторы пользователей положительные, но знаковая колонка принимает всё, что
    принимала unsigned. Существующим установкам правит та же команда max:upgrade: chat_id — пересборкой,
    max_users — шагом change() на месте без переобъявления первичного ключа. Отдельной миграции ради смены типа
    не заводили. Знаковость колонок сама по себе проблему не закрывала: отрицательные идентификаторы отбрасывались
    ещё и фильтрами в MaxChatProfileService, MaxChatsRefreshCommand и MaxUserProfileService, поэтому название
    группы и канала не запрашивалось никогда (в апдейтах его нет, а getChat() — единственный источник), свежая
    отметка chat_checked_at не учитывалась, и профили участников групп и каналов не заполнялись. Фильтры убраны,
    отбрасывается только 0 — такого идентификатора в MAX не бывает.
    Тем же приёмом команда возвращает max_users.phone_verified_at, если колонки нет: её создавала
    аддитивная миграция v1.1.4, у установки v1.1.3 её не будет, а PersistMaxUserPhoneListener пишет отметку
    безусловно — без возврата запись телефона падала бы с «no such column».

Затронутые сценарии

  • Обновление существующей установки: два шага — php artisan migrate (создаёт max_chat_users) и
    php artisan max:upgrade (переносит данные). Данные реестров сохраняются: пары переносятся, дубли чатов сливаются
    по правилам «первые непустые метаданные, max(last_activity_at), min(created_at), статус removed > stopped > active». Между шагами схема неполна — max_chats ещё в прежней форме, а модель уже ждёт новую, поэтому порядок
    обязателен. Команда идемпотентна: повторный запуск на новой форме ничего не меняет.
  • Новая установка: публиковать миграции не нужно, loadMigrationsFrom подхватывает их из пакета; достаточно
    одного php artisan migrate, max:upgrade не требуется.
  • Откат max:upgrade --rollback: симметричен — он тоже пересобирает таблицу, поэтому возвращает и прежнюю форму
    (строка на пару, user_id, счётчик id в auto-increment), а не только набор колонок. user_id становится
    nullable: чат без зафиксированных пар получает строку с NULL — терять факт чата хуже, чем падать на откате. Значения
    прежних ключей как числа не восстанавливаются (они не сохранялись), но повторный запуск команды снова приводит реестр
    к новой форме. Колонка max_users.user_id сужается обратно только если в таблице нет отрицательных
    идентификаторов, — иначе шаг пропускается, потому что сузить их молча означало бы потерять данные.
  • Удаление бота из чата: чат переходит в removed независимо от наличия user в апдейте, и больше не
    опрашивается max:chats:refresh.
  • Диалог и группа: displayName() возвращает title, а при пустом title — имя собеседника или первого
    известного пользователя.
  • Профиль пользователя: MaxUserProfileService::refresh() берёт активные chat_id из max_chats, а
    пользователей — из max_chat_users.
  • Логирование не публикует персональные данные. При явно включённом logging.log_request_body тело апдейта
    с вложением-контактом содержит телефон и содержимое vCard; маскирование знало только про токены, поэтому ключи
    phone, vcf_info и vcf_phone добавлены в список секретных (A09).

Качество

  • PHP 8.4, declare(strict_types=1), PSR-12, PHPStan level max.
  • composer run lint — 0 файлов с правками (65 файлов).
  • vendor/bin/phpstan analyse — 0 ошибок.
  • composer run coverage — 342 теста, 876 утверждений, все зелёные; 97.57% строк (1244/1275),
    88.76% методов, порог 95%.
  • composer audit — 0 уязвимостей при разборе зависимостей; повторный прогон перед выпуском не удался:
    packagist.org в окружении отвечает по таймауту. Зависимости с прошлого прогона не менялись.
  • Guard-тесты схемы: tests/Feature/MaxMigrationsTest.php (форма колонок и индексов, отсутствие аддитивных миграций,
    знаковое объявление chat_id и user_id, round-trip отрицательных идентификаторов во всех трёх реестрах, перенос
    данных не в миграции) и tests/Feature/MaxSchemaUpgradeTest.php (порядок migrate → max:upgrade, перенос данных,
    слияние дублей, приоритет статуса, метаданные, перенос отметки проверки под новым именем вместе со значением,
    установка без колонок метаданных, отбрасывание посторонних колонок прежней схемы, смена первичного ключа
    на chat_id, сохранение знака chat_id при пересборке и откате, расширение max_users.user_id
    с сохранением первичного ключа, отказ от сужения при отрицательных значениях, идемпотентность, частично
    переведённая схема, откат с возвратом счётчика id и повторный запуск, --dry-run, отказ без подтверждения,
    ошибка при отсутствии max_chat_users, возврат max_users.phone_verified_at при его отсутствии и отсутствие
    повторного создания при наличии). Модельные тесты проверяют, что ключ чата — chat_id, а ключ связи — uuid,
    а связи maxUsers/chatUsers/maxChats/chatLinks/maxChat не путают пользователей и чаты при пересечении.
    Отдельно tests/Unit/Support/ConfigEnvCompatibilityTest.php — шесть сценариев совместимости прежнего имени
    переменной на самом файле конфига.
  • Интеграционные смоук-тесты (--group integration) в этом релизе не запускались: нужен реальный MAX_API_TOKEN.