# Luma Luma — рабочий MVP XMPP-клиента для iOS, iPadOS, macOS и Apple Watch с интерфейсом в духе Telegram/iMessage. Клиент не привязан к одному провайдеру: он подключается к обычному XMPP-серверу по JID, включая Prosody. ## Что уже реализовано - вход по JID и паролю; пароль хранится в Apple Keychain; - DNS SRV, STARTTLS и direct TLS, а также ручной host/port; - roster, presence, личные чаты и групповые комнаты XEP-0045 (MUC), включая создание/вход, приглашения, автоподключение и число участников; - отдельный раздел «Контакты»: люди синхронизируются с roster Prosody, а рядом показываются сохранённые MUC-комнаты; есть общий поиск, аватары и статусы; - OMEMO для исходящих и входящих сообщений в личных и неанонимных MUC-чатах, постоянные ключи, TOFU и ручная проверка отпечатков устройств; - глобальный переключатель OMEMO и отдельный режим для каждого чата: наследовать глобальную настройку, всегда шифровать или отправлять без OMEMO; - XEP-0198 Stream Management, XEP-0280 Message Carbons и XEP-0313 MAM; - delivery receipts и локальная история; - редактирование текстовых сообщений через XEP-0308, включая исправления в OMEMO-чатах и синхронизацию через Carbons/MAM; - ответы через XEP-0461 и XEP-0428: Luma показывает исходное сообщение уменьшенным полупрозрачным bubble с дугой и открывает по нажатию фокус-режим всей ветки в стиле iMessage; текстовые `>`-цитаты старых Conversations/Monal также разбираются, а в MUC используется выданный комнатой `stanza-id`; - пересылка текста, геопозиции и медиа в существующий чат или на введённый JID; через пункт «Выбрать» можно отметить несколько сообщений и переслать их в исходном порядке; медиа скачивается/расшифровывается и загружается заново для получателя; - «Удалить у всех» через XEP-0424 Message Retraction с tombstone в истории и отдельное постоянное «Удалить у меня», которое не возвращает сообщение после очередной синхронизации MAM; оба действия поддерживают несколько выбранных сообщений; - съёмка фото прямо системной камерой iPhone, а также выбор фото и видео из медиатеки; фото, видео, музыка и файлы отправляются через XEP-0363 HTTP File Upload; при включённом OMEMO содержимое шифруется до загрузки, а исходное расширение имени сохраняется для совместимости с Conversations и Monal; - множественный выбор до 20 фото/видео или файлов, полноэкранный предпросмотр фото и воспроизведение видео до отправки, удаление отдельных элементов и подпись; после сетевой ошибки неотправленные файлы остаются для повторной попытки, а параллельные загрузки не могут оставить интерфейс заблокированным; - фото и видео показывают inline-превью и по нажатию открываются поверх всего окна приложения; полноэкранный просмотр закрывается вертикальным свайпом без отдельного крестика, увеличенное фото продолжает свободно перемещаться внутри, а видео использует системные элементы воспроизведения; музыка получает inline-плеер с названием файла, длительностью и перемоткой; - каждое фото и видео помещается в отдельный скруглённый островок с внутренним отступом и `aspectFit`: миниатюра не обрезается, не выходит за границы и не слипается с соседними вложениями; - запись голосовых сообщений и круглых видеосообщений (до 60 секунд) с камеры; голосовые показывают waveform, поддерживают паузу, перемотку и скорости 1×/1.5×/2×, а видеосообщения воспроизводятся прямо в круглом bubble, который слегка увеличивается на время воспроизведения; - явно видимый пункт «Отправить геопозицию» в меню `+`, выбор текущей или произвольной точки на карте и отправка стандартного `geo:` URI, распознаваемого другими XMPP-клиентами; - аватары контактов и обновление собственного аватара через XEP-0084 PEP с совместимым fallback на vCard-temp; - личные аудио- и видеозвонки через XMPP Jingle (XEP-0166/XEP-0167), ICE-UDP/DTLS-SRTP и WebRTC; есть входящий экран, mute, динамик, камера и её переключение, а STUN/TURN автоматически берутся через XEP-0215; некорректные ICE-записи от сервера отбрасываются, затем используются публичный STUN и прямые host candidates, поэтому они не блокируют запуск media engine; исходящий `session-accept` нормализуется по порядку media-линий offer; Jingle Message Initiation сразу адресуется выбранному по presence/caps full JID; trickle ICE ждёт удалённый SDP, но больше не зависает в ожидании ресурса, поэтому `transport-info` не попадает на другой endpoint и не остаётся в очереди до timeout; - завершённые аудио- и видеозвонки показываются в истории чата отдельными Telegram-подобными карточками: входящий/исходящий, длительность, отклонённый, пропущенный, отменённый, без ответа или неудачный; пропущенный входящий помечает чат непрочитанным; - у обычных текстовых и файловых сообщений используются чистые скруглённые островки без «хвостиков»; - единый заполненный `AppAssets.xcassets/AppIcon` со всеми 42 размерами для iPhone, iPad, Mac и Apple Watch; один и тот же catalog явно подключён ко всем app-target’ам, а имя закреплено в build settings и Info.plist; - адаптивный SwiftUI-интерфейс для iPhone, iPad и Mac; - companion-приложение watchOS: последние чаты, диктовка/ответы, а также запись голосового до 60 секунд с отменой и предпрослушиванием; `.m4a` передаётся на iPhone фоновой файловой очередью WatchConnectivity, после чего iPhone отправляет его через обычный XEP-0363/OMEMO media-пайплайн и возвращает часам итоговый статус; это современный single-target watchOS app, поэтому SwiftUI-код и executable собираются в одном target без устаревшего WatchKit App/Extension контейнера; - системные уведомления для новых сообщений, пока приложение запущено в фоне или на экране; в открытом сейчас диалоге дублирующий banner не показывается. ## Сборка Нужны macOS, Xcode 16 или новее и XcodeGen. ```bash brew install xcodegen cd Luma make project open Luma.xcodeproj ``` После обновления архива обязательно снова выполните `make project`: новые build settings AppIcon берутся из `project.yml`. Если устройство или Simulator закэшировали старую пустую иконку, один раз удалите установленную Luma и установите её заново; на macOS после новой сборки перезапустите Dock или сам Mac. Можно вместо команд дважды нажать `Regenerate-Luma-Project.command` в корне проекта (XcodeGen всё равно должен быть установлен). При переходе с версии 0.8.7 один раз выполните в Xcode **Product → Clean Build Folder**, чтобы удалить кэш старого `application.watchapp2` target. После открытия проекта: 1. Выберите свою Apple Development Team для `Luma`, `LumaMac` и `LumaWatch`. 2. При необходимости замените bundle identifiers `app.luma.chat*` в `project.yml` на уникальные. 3. Дождитесь загрузки Swift packages. 4. Запустите scheme `Luma` на iPhone/iPad или `LumaMac` на Mac. Для проверки из терминала на Mac: ```bash make verify ``` Версии Martin, MartinOMEMO и WebRTC закреплены в `project.yml`. Зависимости Martin имеют copyleft-лицензии, поэтому Luma распространяется под AGPL-3.0-or-later. ## Prosody Минимально нужны `pep`, `smacks`, `carbons`, `mam` и MUC component. `pep` используется и для OMEMO, и для XEP-0084 аватаров. Для медиа и вложений добавьте компонент `http_file_share`; для надёжных звонков за NAT — coturn и `turn_external`. Готовый пример и команды проверки находятся в [`Docs/PROSODY.md`](Docs/PROSODY.md). ## Важные границы MVP - OMEMO-модуль использует широко развёрнутый профиль с namespace `eu.siacs.conversations.axolotl`, совместимый со многими версиями Monal и Conversations. Поддержку нового namespace `urn:xmpp:omemo:2` следует добавить отдельным криптографическим адаптером до объявления полной совместимости с актуальным XEP-0384. - Надёжный push при выгруженном iOS-приложении не может работать только через XMPP-сокет. Для production нужны Apple Developer credentials, APNs provider, XEP-0357 push-компонент и регистрация приложения в этом компоненте. - При первом запуске Luma запрашивает ограниченную последнюю страницу серверного MAM-архива, чтобы огромная история не блокировала интерфейс, и показывает индикатор «Синхронизация истории…» только на этом первичном этапе. После успешного прохода сохраняются серверный UID последней записи и время; следующие подключения продолжают RSM строго после UID. Временное перекрытие используется только для старых snapshots без UID или один раз, если сервер удалил сохранённый UID. Сырые stanza собираются вне main queue, а расшифровка OMEMO выполняется на отдельной фоновой очереди — главный поток освобождается на время Signal-криптографии, поэтому большая история не лагает интерфейс. Инкрементальная догрузка backlog продолжается в фоне без вечного спиннера, а сообщения применяются атомарными пачками с дедупликацией по `origin-id`/stanza id через хеш-индексы. После сбоя отдельной страницы синхронизация автоматически повторяется ограниченное число раз, не дожидаясь повторного открытия приложения. - Клиент не вводит собственного ограничения размера вложения: фактический предел сообщает XEP-0363 upload-компонент сервера. Видеосообщение ограничено 60 секундами. - XEP-0424 retract является запросом на удаление: федеративный XMPP не может гарантировать стирание уже полученной или скопированной версии в чужом клиенте. «Удалить у меня» влияет только на это устройство. - Групповое OMEMO требует неанонимную MUC-комнату и OMEMO-устройство у каждого получателя. Luma создаёт новые комнаты members-only/persistent с `muc#roomconfig_whois=anyone`; для несовместимой чужой комнаты безопасная отправка блокируется без автоматического перехода на plaintext. Сервер также должен разрешать участникам читать списки member/admin/owner. - Звонки в MVP работают один-на-один, пока приложение запущено и XMPP-соединение активно. WebRTC защищает медиапоток DTLS-SRTP, но это отдельный механизм от OMEMO. Для надёжного входящего звонка после выгрузки iOS-приложения нужны PushKit/CallKit, APNs provider и серверный push-компонент. Карточка звонка хранится в локальной истории участвовавшего устройства Luma; она не отправляется собеседнику как фальшивое текстовое сообщение и поэтому не синхронизируется через MAM. - Реакции и групповые звонки относятся к следующему этапу. Архитектура описана в [`Docs/ARCHITECTURE.md`](Docs/ARCHITECTURE.md), модель угроз и ограничения хранения — в [`Docs/SECURITY.md`](Docs/SECURITY.md).