Пуши из мессенджера, которого нет в App Store
Задача
Мессенджер MAX убрали из App Store. Установленное приложение на iPhone осталось и работает, но пуши через APNs до телефона больше не доходят. О новом сообщении узнаёшь ровно одним способом – зайдя в приложение и посмотрев.
Для личной переписки это терпимо. Для переписки, где иногда бывает срочное, – нет. Задача сформулировалась сама: узнавать о новом сообщении в MAX так же быстро, как о сообщении в Telegram, при этом ничего не ломая в самом мессенджере и не занимаясь реверс-инжинирингом его протокола.
Где искать точку входа
Первая мысль была плохая: поднять клиент на виртуальной машине и снимать экран, распознавая всплывающие плашки. Работало бы, но хрупко до неприличия – любое изменение вёрстки, и всё разваливается.
Правильный ответ нашёлся, когда я вспомнил, что у MAX есть десктоп-клиент под Linux.
А под Linux уведомление – это не картинка на экране. Это вызов метода Notify()
по D-Bus на имени org.freedesktop.Notifications. Приложение не рисует плашку само,
оно просит систему её нарисовать и передаёт при этом заголовок и текст.
Обычно на том конце сидит dunst или оболочка рабочего стола. Но имя на шине не
зарезервировано – его занимает тот, кто пришёл первым. Значит, вместо рисовалки плашек
там может сидеть мой сервис, и тогда summary и body вызова – это ровно
«от кого» и «что написал», уже разобранные, в структурированном виде.
Ни парсинга экрана, ни разбора сетевого протокола. Приложение само отдаёт нужное, причём по документированному интерфейсу, который не менялся лет пятнадцать.
Отдельно я рассматривал доставку не в Telegram, а в iMessage – казалось изящным дотянуться до родных уведомлений телефона. Отказался быстро: для этого нужен постоянно включённый мак с открытой сессией, то есть ещё одна машина в контуре и ещё одна точка отказа. Telegram в этой роли скучнее, но у него живые пуши, он уже есть на телефоне и он мне ничего не должен.
Что получилось
На домашнем сервере крутится контейнер. Внутри – официальный клиент MAX из его apt-репозитория с запиненной версией, и всё, что нужно, чтобы графическое приложение поверило, что оно на настоящем рабочем столе:
- Xvfb – виртуальный экран, потому что настоящего нет;
- openbox – оконный менеджер, без него клиент ведёт себя странно;
- gnome-keyring в режиме одних секретов – клиент хранит учётные данные через libsecret
и без
org.freedesktop.secretsпадает ещё до окна входа; - сессионная шина D-Bus, на которой мой сервис занимает имя уведомлений.
Порядок запуска здесь не косметика. Сервис уведомлений обязан занять имя до старта клиента: если в момент запуска имя свободно, клиент решает, что показывать уведомления некому, и больше к этому вопросу не возвращается.
Вход в аккаунт – один раз по QR-коду. Экрана нет, поэтому окно клиента прокидывается наружу через noVNC: открываешь страницу в браузере, наводишь камеру телефона на QR в окне. Сессия лежит в примонтированном томе и переживает и перезапуск, и пересборку образа.
Доставка
Дальше событие надо донести до телефона, а это отдельная история. Отправлять
в Telegram прямо с домашнего сервера не выходит: из России api.telegram.org под ТСПУ
рвётся. Проверено не в теории – ровно на этом в своё время сломался мой нотификатор,
и его пришлось переселять на VPS в Амстердаме.
Поэтому схема двухступенчатая. Домашний сервер по SSH кладёт событие в очередь на VPS, а уже VPS отправляет его в Telegram.
Ключ для этой доставки ограничен forced command и умеет ровно одно действие: принять на вход не больше 64 КБ и положить их файлом в конкретный каталог очереди. Ни имя файла, ни путь домашний сервер не выбирает – это решает принимающая сторона. Если контейнер когда-нибудь скомпрометируют, максимум, что можно сделать этим ключом, – насыпать мусора в одну папку.
Очередь на VPS разгребает отдельный поток раз в две секунды. У нотификатора там есть и обычное расписание, но пятиминутный круг сообщениям из мессенджера не годится: уведомление, приехавшее через пять минут, – это уже не уведомление.
Сторож
Самая интересная часть проекта – не перехват, а мониторинг, и вот почему.
Такая конструкция ломается не громко, а тихо. Сессия клиента рано или поздно протухает. Клиент показывает экран входа и просто перестаёт слать уведомления. Ошибок нет, процесс жив, контейнер здоров, healthcheck зелёный. Снаружи это абсолютно неотличимо от спокойного дня, когда вам никто не написал.
Значит, следить надо не за отсутствием ошибок, а за прямыми признаками жизни. Сторож проверяет четыре вещи:
- Экран входа вместо чатов – по снимку виртуального экрана. QR-код это большое чёрно-белое пятно в центре окна, и он отлично считается простой метрикой: доля тёмного в центральной области. На экране входа она около 0,245, на рабочих экранах не выше 0,043. Порог поставлен на 0,12 – с большим запасом в обе стороны.
- Имена на шине. Если сервис уведомлений или хранилище секретов отвалились, клиент физически не сможет ничего прислать.
- Жив ли процесс клиента.
- Не застряло ли событие в очереди дольше десяти минут.
Пара деталей, которые пришлось добавить после первых ложных тревог. Отдельная опорная полоса ниже центра отсекает срабатывание на чёрном экране, пока клиент ещё не успел нарисовать окно после старта. И поломка объявляется только после двух подтверждений подряд, а возврат в норму – сразу: ложная тревога дороже, чем минута задержки.
Пороги замерены на реальных снимках, а каждый осмотр пишется в историю – через пару недель их можно уточнить по фактическому разбросу, а не по четырём картинкам, снятым в день разработки.
И главное правило всей этой конструкции: сообщение о том, что сломался канал уведомлений, не должно ехать по этому же каналу. Вердикт сторожа забирает независимая проверка здоровья домашнего сервера, а тревогу поднимает нотификатор по своей собственной линии.
Тревогу «давно никто не писал» я, кстати, выключил. Сутки тишины в личном мессенджере – это выходные, а не поломка.
Эксплуатация
Пара вещей, о которых стоит подумать заранее, если будете делать похожее.
Версия клиента запинена. Обновлять графическое приложение, которое живёт без присмотра и держит сессию, автоматически – плохая идея: новая версия может поменять поведение при старте, потребовать переавторизации или просто не подняться, и узнаете вы об этом по тишине. Версия зафиксирована, обновление – осознанное действие руками. Автоматические апдейтеры контейнеров про пакет внутри образа не знают и не подскажут, так что новые версии приходится проверять самому.
Сырой поток уведомлений пишется в лог до всякой фильтрации. Это выглядит избыточным ровно до первого «а почему это сообщение не приехало». Имея сырой поток, вопрос закрывается за минуту: либо события не было вовсе (проблема на стороне клиента), либо оно было и потерялось дальше по цепочке. Без него это гадание.
Смотреть, а не гадать. У контейнера есть команда, снимающая скриншот виртуального экрана. Звучит несерьёзно, но именно она сэкономила больше всего времени: когда клиент ведёт себя странно, гораздо быстрее один раз посмотреть, что у него на экране, чем строить теории по логам.
Выводы
Ищите интерфейс, который уже есть. Задача выглядела как «распознать плашку на экране», а оказалась «занять имя на шине». Разница между хрупким хаком и надёжной конструкцией – ровно в том, нашли вы существующий контракт или придумали свой поверх картинки.
Права выдавайте по одной операции, а не по одной машине. Не «ключ от сервера», а «ключ, умеющий положить файл в эту очередь». Формулируется в одну строку конфига, а класс возможных проблем закрывает целиком.
Молчащая система – самая опасная. Особенно та, чья нормальная работа тоже выглядит как тишина. Проверять надо признаки жизни, а не отсутствие ошибок, и проверять чем-то внешним по отношению к системе.
Пороги замеряйте, а не выдумывайте. «Доля тёмного больше 0,12» – это не угаданное число, а середина между двумя измеренными кучами. И пишите историю измерений с первого дня: через месяц она стоит дороже, чем аккуратность в день запуска.