Repository navigation
v1.2.0
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.