182 lines
16 KiB
Markdown
182 lines
16 KiB
Markdown
# 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;
|
||
- поддержка портретной и альбомной ориентации на iPhone и всех четырёх ориентаций на iPad;
|
||
- 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).
|