# Jabogram: Дизайн-документ > Версия: 1.0 > Дата: 2026-02-10 --- ## Содержание 1. [Введение](#1-введение) 2. [Глоссарий](#2-глоссарий) 3. [Архитектура системы](#3-архитектура-системы) 4. [Структура проекта](#4-структура-проекта) 5. [Сервер](#5-сервер) 6. [Протокол XMPP: используемые XEP](#6-протокол-xmpp-используемые-xep) 7. [Формат сообщений](#7-формат-сообщений) 8. [Клиентское приложение — общая библиотека](#8-клиентское-приложение--общая-библиотека) 9. [Клиентское приложение — iOS](#9-клиентское-приложение--ios) 10. [Клиентское приложение — Android](#10-клиентское-приложение--android) 11. [Модели данных](#11-модели-данных) 12. [Сценарии взаимодействия](#12-сценарии-взаимодействия) 13. [Безопасность](#13-безопасность) 14. [Развёртывание](#14-развёртывание) 15. [Тестирование](#15-тестирование) 16. [Ограничения и допущения v1](#16-ограничения-и-допущения-v1) --- ## 1. Введение ### 1.1 Цель Jabogram — мессенджер для обмена **текстовыми** и **голосовыми** сообщениями (голос — только предзаписанные аудиофайлы, без реалтайм-стриминга). Протокол — XMPP. ### 1.2 Основные возможности (scope v1) | Функция | Описание | |---|---| | Регистрация / вход | XMPP In-Band Registration (XEP-0077) + SASL-аутентификация | | Личные чаты (1-on-1) | Текст и голосовые сообщения | | Групповые чаты | Multi-User Chat (XEP-0045) — текст и голосовые сообщения | | Голосовые сообщения | Запись аудио → загрузка файла → отправка ссылки через XMPP | | Статусы сообщений | Отправлено / доставлено / прочитано | | Индикатор набора | «Пользователь печатает…» | | Список контактов | XMPP Roster + поиск по JID | | Присутствие | Online / Away / Offline | | История сообщений | MAM (XEP-0313) + локальный кеш SQLite | | Push-уведомления | APNs (iOS), FCM (Android) через XEP-0357 | | Профиль пользователя | Никнейм + аватар (XEP-0084) | ### 1.3 Вне scope v1 - Голосовые/видеозвонки (WebRTC) - End-to-end шифрование (OMEMO, XEP-0384) - Передача произвольных файлов (изображения, документы) - Web-клиент - Стикеры / реакции --- ## 2. Глоссарий | Термин | Определение | |---|---| | **JID** | Jabber ID — адрес пользователя в XMPP, формат `user@domain/resource` | | **Bare JID** | JID без resource-части: `user@domain` | | **Full JID** | JID с resource: `user@domain/phone` | | **Roster** | Список контактов пользователя, хранится на сервере | | **Stanza** | XML-сообщение протокола XMPP (``, ``, ``) | | **MAM** | Message Archive Management — серверный архив сообщений | | **MUC** | Multi-User Chat — групповой чат XMPP | | **XEP** | XMPP Extension Protocol — расширение протокола | | **OOB** | Out of Band Data — передача URL файла внутри stanza | | **BOSH** | Bidirectional-streams Over Synchronous HTTP (не используется, только WebSocket) | --- ## 3. Архитектура системы ### 3.1 Общая схема ``` ┌──────────────┐ WSS (5443) ┌──────────────────────┐ │ iOS-клиент │◄─────────────────────────► │ │ │ (React │ │ ejabberd │ │ Native) │ │ XMPP-сервер │ └──────────────┘ │ │ │ ┌────────────────┐ │ ┌──────────────┐ WSS (5443) │ │ mod_mam │ │ ┌────────────┐ │ Android- │◄─────────────────────────► │ │ mod_muc │ │◄──►│ PostgreSQL │ │ клиент │ │ │ mod_http_upload│ │ └────────────┘ │ (React │ │ │ mod_push │ │ │ Native) │ │ │ mod_roster │ │ ┌────────────┐ └──────────────┘ │ │ ... │ │───►│ File │ │ └────────────────┘ │ │ Storage │ └──────────┬───────────┘ └────────────┘ │ ┌──────────▼───────────┐ │ Push Gateway │ │ (APNs / FCM) │ └──────────────────────┘ ``` ### 3.2 Технологический стек | Компонент | Технология | |---|---| | XMPP-сервер | **ejabberd** (latest stable) | | БД сервера | **PostgreSQL 16+** | | Файловое хранилище | Локальный диск / S3-совместимое (MinIO) | | Reverse proxy | **Nginx** (TLS-терминация, проксирование WebSocket) | | Контейнеризация | **Docker + docker-compose** | | Клиент: фреймворк | **React Native** (latest stable, New Architecture) | | Клиент: XMPP-библиотека | **stanza.js** | | Клиент: навигация | **React Navigation 7+** | | Клиент: состояние | **Zustand** | | Клиент: локальная БД | **op-sqlite** (SQLite для React Native) | | Клиент: аудио | **expo-av** или **react-native-audio-recorder-player** | | Клиент: push (iOS) | **@react-native-firebase/messaging** + APNs | | Клиент: push (Android) | **@react-native-firebase/messaging** + FCM | | Язык клиента | **TypeScript** (strict mode) | --- ## 4. Структура проекта Монорепозиторий с npm/yarn workspaces. ``` jabogram/ ├── README.md ├── package.json # workspace root ├── doc/ │ └── DESIGN.md # этот документ │ ├── server/ │ ├── docker-compose.yml # ejabberd + postgres + nginx + minio │ ├── ejabberd/ │ │ ├── Dockerfile # кастомный образ (если нужны плагины) │ │ ├── ejabberd.yml # основной конфиг │ │ └── certs/ # TLS-сертификаты (не в git) │ ├── nginx/ │ │ └── nginx.conf # reverse proxy конфиг │ ├── postgres/ │ │ └── init.sql # начальная схема (если нужна) │ └── scripts/ │ ├── deploy.sh # скрипт развёртывания │ ├── backup.sh # бекап БД │ └── create-user.sh # создание пользователя через ejabberdctl │ ├── react-native-lib/ │ ├── package.json │ ├── tsconfig.json │ ├── src/ │ │ ├── index.ts # public API библиотеки │ │ ├── services/ │ │ │ ├── xmpp/ │ │ │ │ ├── XMPPClient.ts # подключение, авторизация, реконнект │ │ │ │ ├── MessageService.ts # отправка/приём сообщений │ │ │ │ ├── RosterService.ts # управление контактами │ │ │ │ ├── PresenceService.ts # статусы присутствия │ │ │ │ ├── MUCService.ts # групповые чаты │ │ │ │ ├── MAMService.ts # архив сообщений │ │ │ │ ├── FileUploadService.ts # XEP-0363 HTTP File Upload │ │ │ │ └── types.ts # XMPP-специфичные типы │ │ │ ├── audio/ │ │ │ │ ├── AudioRecorder.ts # запись голосового сообщения │ │ │ │ └── AudioPlayer.ts # воспроизведение │ │ │ ├── push/ │ │ │ │ └── PushService.ts # регистрация push-токена │ │ │ └── storage/ │ │ │ ├── Database.ts # инициализация SQLite │ │ │ ├── MessageRepository.ts # CRUD сообщений │ │ │ ├── ChatRepository.ts # CRUD чатов │ │ │ └── ContactRepository.ts # CRUD контактов │ │ ├── stores/ │ │ │ ├── useAuthStore.ts │ │ │ ├── useChatsStore.ts │ │ │ ├── useContactsStore.ts │ │ │ ├── usePresenceStore.ts │ │ │ └── useSettingsStore.ts │ │ ├── screens/ │ │ │ ├── auth/ │ │ │ │ ├── LoginScreen.tsx │ │ │ │ └── RegisterScreen.tsx │ │ │ ├── chats/ │ │ │ │ ├── ChatListScreen.tsx │ │ │ │ └── ChatScreen.tsx │ │ │ ├── contacts/ │ │ │ │ ├── ContactsScreen.tsx │ │ │ │ └── AddContactScreen.tsx │ │ │ ├── groups/ │ │ │ │ ├── CreateGroupScreen.tsx │ │ │ │ └── GroupInfoScreen.tsx │ │ │ └── profile/ │ │ │ └── ProfileScreen.tsx │ │ ├── components/ │ │ │ ├── MessageBubble.tsx │ │ │ ├── VoiceMessageBubble.tsx │ │ │ ├── ChatInput.tsx │ │ │ ├── VoiceRecordButton.tsx │ │ │ ├── VoiceRecordOverlay.tsx │ │ │ ├── ChatListItem.tsx │ │ │ ├── ContactListItem.tsx │ │ │ ├── Avatar.tsx │ │ │ ├── PresenceDot.tsx │ │ │ ├── TypingIndicator.tsx │ │ │ └── MessageStatus.tsx │ │ ├── navigation/ │ │ │ └── AppNavigator.tsx │ │ ├── hooks/ │ │ │ ├── useXMPP.ts │ │ │ ├── useAudioRecorder.ts │ │ │ ├── useAudioPlayer.ts │ │ │ └── useMessages.ts │ │ ├── types/ │ │ │ └── index.ts │ │ └── utils/ │ │ ├── jid.ts # парсинг/форматирование JID │ │ ├── datetime.ts # форматирование дат │ │ └── audio.ts # утилиты для аудио │ └── __tests__/ │ └── ... │ ├── react-native-ios/ │ ├── package.json # зависимость на react-native-lib │ ├── index.js # точка входа (импорт App из lib) │ ├── app.json │ ├── metro.config.js # пути к workspace-пакетам │ ├── babel.config.js │ ├── Gemfile # CocoaPods │ └── ios/ │ ├── Jabogram/ │ │ ├── AppDelegate.mm │ │ ├── Info.plist # permissions, URL schemes, background modes │ │ └── Jabogram.entitlements # push notifications entitlement │ ├── Jabogram.xcodeproj/ │ └── Podfile │ └── react-native-android/ ├── package.json # зависимость на react-native-lib ├── index.js # точка входа (импорт App из lib) ├── app.json ├── metro.config.js ├── babel.config.js └── android/ ├── app/ │ ├── src/main/ │ │ ├── AndroidManifest.xml # permissions, services │ │ ├── java/.../ │ │ │ └── MainActivity.kt │ │ └── res/ # иконки, цвета, строки │ └── build.gradle.kts ├── build.gradle.kts ├── settings.gradle.kts └── google-services.json # FCM (не в git) ``` ### 4.1 Workspace-конфигурация (корневой package.json) ```jsonc { "name": "jabogram", "private": true, "workspaces": [ "react-native-lib", "react-native-ios", "react-native-android" ] } ``` ### 4.2 Metro-конфигурация (react-native-ios и react-native-android) Обе платформенные директории должны иметь `metro.config.js`, который умеет резолвить модули из `react-native-lib`: ```js const path = require('path'); const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config'); const libPath = path.resolve(__dirname, '../react-native-lib'); const config = { watchFolders: [libPath], resolver: { nodeModulesPaths: [ path.resolve(__dirname, 'node_modules'), path.resolve(libPath, 'node_modules'), ], }, }; module.exports = mergeConfig(getDefaultConfig(__dirname), config); ``` --- ## 5. Сервер ### 5.1 Выбор: ejabberd Почему ejabberd: - Erlang/OTP — высокая производительность и отказоустойчивость - Поддержка всех необходимых XEP «из коробки» - Встроенный `mod_http_upload` для загрузки файлов - Встроенный `mod_push` для push-уведомлений - REST API для администрирования - Официальный Docker-образ - Поддержка PostgreSQL в качестве бэкенда ### 5.2 docker-compose.yml ```yaml version: "3.8" services: ejabberd: image: ejabberd/ecs:latest container_name: jabogram-ejabberd restart: unless-stopped ports: - "5222:5222" # XMPP client-to-server (STARTTLS) - "5443:5443" # XMPP over WebSocket (WSS) + HTTP Upload - "5280:5280" # Admin web UI + API (только localhost) volumes: - ./ejabberd/ejabberd.yml:/home/ejabberd/conf/ejabberd.yml:ro - ./ejabberd/certs:/home/ejabberd/certs:ro - ejabberd-database:/home/ejabberd/database - ejabberd-uploads:/home/ejabberd/uploads depends_on: postgres: condition: service_healthy environment: - ERLANG_NODE_ARG=ejabberd@localhost - CTL_ON_CREATE=register admin jabogram.example.com adminpassword postgres: image: postgres:16-alpine container_name: jabogram-postgres restart: unless-stopped environment: POSTGRES_DB: ejabberd POSTGRES_USER: ejabberd POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" # из .env volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ejabberd"] interval: 5s timeout: 3s retries: 5 nginx: image: nginx:alpine container_name: jabogram-nginx restart: unless-stopped ports: - "443:443" volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./ejabberd/certs:/etc/nginx/certs:ro depends_on: - ejabberd volumes: postgres-data: ejabberd-database: ejabberd-uploads: ``` ### 5.3 ejabberd.yml — основная конфигурация ```yaml hosts: - jabogram.example.com loglevel: info certfiles: - /home/ejabberd/certs/*.pem ## ─── Listeners ──────────────────────────────────────────── listen: # XMPP client-to-server - port: 5222 ip: "::" module: ejabberd_c2s max_stanza_size: 262144 shaper: c2s_shaper access: c2s starttls_required: true # XMPP over WebSocket + HTTP Upload + BOSH (fallback) - port: 5443 ip: "::" module: ejabberd_http tls: true request_handlers: /ws: ejabberd_http_ws # WebSocket endpoint /upload: mod_http_upload # HTTP File Upload /api: mod_http_api # REST API # Admin interface (привязан к localhost) - port: 5280 ip: "127.0.0.1" module: ejabberd_http request_handlers: /admin: ejabberd_web_admin /api: mod_http_api ## ─── Authentication ─────────────────────────────────────── auth_method: sql auth_password_format: scram ## ─── Database ───────────────────────────────────────────── default_db: sql new_sql_schema: true sql_type: pgsql sql_server: postgres sql_port: 5432 sql_database: ejabberd sql_username: ejabberd sql_password: "${POSTGRES_PASSWORD}" sql_pool_size: 10 ## ─── Modules ────────────────────────────────────────────── modules: ## Roster (список контактов) mod_roster: db_type: sql versioning: true ## Presence (онлайн-статус) mod_presence: {} ## Одиночные чаты — карбоны (синхронизация между устройствами) mod_carboncopy: {} ## Статусы доставки сообщений mod_receipt: {} ## Индикатор набора текста mod_chatstate: {} ## Архив сообщений (история) mod_mam: db_type: sql default: always # архивировать все сообщения request_activates_archiving: false assume_mam_usage: true compress_xml: true ## Stream Management (надёжная доставка, возобновление сессии) mod_stream_mgmt: resend_on_timeout: if_offline ## Групповые чаты mod_muc: db_type: sql access_create: muc_create default_room_options: persistent: true mam: true allow_change_subj: true max_users: 200 ## HTTP File Upload (для голосовых сообщений) mod_http_upload: put_url: "https://jabogram.example.com:5443/upload" get_url: "https://jabogram.example.com:5443/upload" docroot: /home/ejabberd/uploads max_size: 10485760 # 10 MB file_mode: "0640" dir_mode: "2750" thumbnail: false custom_headers: "Access-Control-Allow-Origin": "*" "Access-Control-Allow-Headers": "Content-Type" ## In-Band Registration (регистрация через XMPP) mod_register: access: register ip_access: trusted_network welcome_message: subject: "Welcome to Jabogram!" body: "Welcome!" ## Push-уведомления (XEP-0357) mod_push: {} mod_push_keepalive: {} ## vCard (профиль пользователя) mod_vcard: db_type: sql ## Аватар пользователя mod_avatar: {} ## Пинг (keepalive) mod_ping: send_pings: true ping_interval: 60 ping_ack_timeout: 30 timeout_action: kill ## Блокировка пользователей mod_blocking: db_type: sql ## Service Discovery mod_disco: {} ## Время последней активности mod_last: db_type: sql ## Приватное хранилище (для клиентских настроек) mod_private: db_type: sql ## Версия сервера mod_version: {} ## ─── ACL / Access ───────────────────────────────────────── acl: admin: user: admin@jabogram.example.com local: user_regexp: "" access_rules: c2s: allow: all register: allow: all muc_create: allow: local shaper_rules: c2s_shaper: none: admin normal: all shaper: normal: rate: 10000 burst_size: 50000 ``` ### 5.4 nginx.conf ```nginx events { worker_connections 1024; } http { upstream ejabberd_ws { server ejabberd:5443; } server { listen 443 ssl; server_name jabogram.example.com; ssl_certificate /etc/nginx/certs/fullchain.pem; ssl_certificate_key /etc/nginx/certs/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; # WebSocket proxy location /ws { proxy_pass https://ejabberd_ws/ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_read_timeout 600s; proxy_send_timeout 600s; } # HTTP File Upload location /upload { proxy_pass https://ejabberd_ws/upload; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; client_max_body_size 10m; } } } ``` ### 5.5 Скрипты управления **scripts/deploy.sh:** ```bash #!/usr/bin/env bash set -euo pipefail cd "$(dirname "$0")/.." docker compose up -d --build docker compose exec ejabberd ejabberdctl status ``` **scripts/create-user.sh:** ```bash #!/usr/bin/env bash # Использование: ./create-user.sh username password set -euo pipefail USER="$1" PASS="$2" DOMAIN="jabogram.example.com" docker compose exec ejabberd ejabberdctl register "$USER" "$DOMAIN" "$PASS" echo "Created: ${USER}@${DOMAIN}" ``` --- ## 6. Протокол XMPP: используемые XEP | XEP | Название | Назначение в Jabogram | |---|---|---| | [XEP-0077](https://xmpp.org/extensions/xep-0077.html) | In-Band Registration | Регистрация нового аккаунта прямо через XMPP-соединение | | [XEP-0030](https://xmpp.org/extensions/xep-0030.html) | Service Discovery | Обнаружение возможностей сервера (поддержка upload, MUC и т.д.) | | [XEP-0045](https://xmpp.org/extensions/xep-0045.html) | Multi-User Chat | Групповые чаты | | [XEP-0066](https://xmpp.org/extensions/xep-0066.html) | Out of Band Data | Передача URL голосового файла в теле сообщения | | [XEP-0084](https://xmpp.org/extensions/xep-0084.html) | User Avatar | Аватары пользователей | | [XEP-0085](https://xmpp.org/extensions/xep-0085.html) | Chat State Notifications | Индикатор «печатает…» | | [XEP-0184](https://xmpp.org/extensions/xep-0184.html) | Message Delivery Receipts | Подтверждение доставки (✓✓) | | [XEP-0191](https://xmpp.org/extensions/xep-0191.html) | Blocking Command | Блокировка пользователей | | [XEP-0198](https://xmpp.org/extensions/xep-0198.html) | Stream Management | Надёжная доставка stanza, возобновление сессии после обрыва | | [XEP-0280](https://xmpp.org/extensions/xep-0280.html) | Message Carbons | Синхронизация сообщений между устройствами одного пользователя | | [XEP-0313](https://xmpp.org/extensions/xep-0313.html) | Message Archive Management | Серверный архив; синхронизация истории при подключении | | [XEP-0333](https://xmpp.org/extensions/xep-0333.html) | Chat Markers | Статус «прочитано» (displayed marker) | | [XEP-0352](https://xmpp.org/extensions/xep-0352.html) | Client State Indication | Клиент сообщает серверу, что ушёл в фон (экономия трафика) | | [XEP-0357](https://xmpp.org/extensions/xep-0357.html) | Push Notifications | Серверные push-уведомления при офлайн-сообщениях | | [XEP-0363](https://xmpp.org/extensions/xep-0363.html) | HTTP File Upload | Загрузка голосовых файлов на сервер | --- ## 7. Формат сообщений ### 7.1 Текстовое сообщение (1-on-1) ```xml Привет, как дела? ``` ### 7.2 Подтверждение доставки (XEP-0184) ```xml ``` ### 7.3 Маркер «прочитано» (XEP-0333) ```xml ``` ### 7.4 Индикатор набора текста (XEP-0085) ```xml ``` ### 7.5 Голосовое сообщение Голосовое сообщение отправляется в два этапа: **Этап 1 — получение upload-слота (XEP-0363):** Запрос: ```xml ``` Ответ сервера: ```xml
Basic ...
audio/mp4
``` **Этап 2 — загрузка файла по HTTP PUT:** ``` PUT /upload/abcdef123/voice-1707580800.m4a HTTP/1.1 Host: jabogram.example.com:5443 Content-Type: audio/mp4 Content-Length: 48320 ``` **Этап 3 — отправка XMPP-сообщения со ссылкой:** ```xml https://jabogram.example.com:5443/upload/abcdef123/voice-1707580800.m4a https://jabogram.example.com:5443/upload/abcdef123/voice-1707580800.m4a voice-message ``` Пояснение полей ``: | Атрибут | Тип | Описание | |---|---|---| | `duration` | int | Длительность в секундах | | `mime-type` | string | MIME-тип аудио (`audio/mp4`) | | `size` | int | Размер файла в байтах | | `waveform` | string | CSV нормализованных float-значений (0..1) для визуализации волны, ~10-50 точек | Клиент-получатель: 1. Видит `` → рисует UI голосового сообщения с waveform 2. По нажатию Play → скачивает файл по URL из `` → воспроизводит 3. Кеширует скачанный файл локально Если клиент не поддерживает голосовые сообщения — показывает `` как обычную ссылку (graceful degradation). ### 7.6 Сообщение в групповой чат (MUC) ```xml Всем привет! ``` --- ## 8. Клиентское приложение — общая библиотека ### 8.1 XMPP-клиент (services/xmpp/XMPPClient.ts) Обёртка над stanza.js. Основные обязанности: ```typescript interface XMPPClientConfig { domain: string; // jabogram.example.com wsURL: string; // wss://jabogram.example.com/ws resource: string; // "ios" | "android" } class XMPPClient { // Жизненный цикл connect(jid: string, password: string): Promise; disconnect(): Promise; reconnect(): Promise; // Stream Management (XEP-0198) enableStreamManagement(): void; resumeSession(): Promise; // Client State Indication (XEP-0352) setActive(): void; setInactive(): void; // Низкоуровневый доступ sendStanza(stanza: Element): void; onStanza(handler: (stanza: Element) => void): void; // События on(event: 'connected' | 'disconnected' | 'error' | 'stanza', cb): void; } ``` **Стратегия реконнекта:** 1. При потере соединения — попытка resume (XEP-0198) 2. Если resume не удался — полный reconnect 3. Экспоненциальный backoff: 1s → 2s → 4s → 8s → 16s → 30s (max) 4. При переходе приложения из background в foreground — немедленная попытка reconnect ### 8.2 Сервис сообщений (services/xmpp/MessageService.ts) ```typescript class MessageService { sendTextMessage(to: string, body: string, chatType: 'chat' | 'groupchat'): Message; sendVoiceMessage(to: string, audioFile: AudioFile, chatType: 'chat' | 'groupchat'): Promise; sendDeliveryReceipt(to: string, messageId: string): void; sendDisplayedMarker(to: string, messageId: string): void; sendChatState(to: string, state: ChatState): void; onMessage(handler: (message: Message) => void): void; onReceipt(handler: (messageId: string) => void): void; onDisplayed(handler: (messageId: string) => void): void; onChatState(handler: (jid: string, state: ChatState) => void): void; } type ChatState = 'active' | 'composing' | 'paused' | 'inactive' | 'gone'; ``` ### 8.3 Сервис загрузки файлов (services/xmpp/FileUploadService.ts) ```typescript class FileUploadService { /** * 1. Запрашивает upload slot (XEP-0363) * 2. Загружает файл по HTTP PUT * 3. Возвращает GET URL для вставки в сообщение */ async uploadFile(file: { uri: string; name: string; size: number; mimeType: string; }): Promise<{ getUrl: string }>; } ``` ### 8.4 Сервис аудио (services/audio/) ```typescript // AudioRecorder.ts class AudioRecorder { /** Начать запись. Формат: AAC (.m4a) */ async start(): Promise; /** Остановить запись и вернуть файл */ async stop(): Promise; /** Отменить запись */ async cancel(): Promise; /** Текущая амплитуда (для анимации) */ onAmplitude(handler: (amplitude: number) => void): void; /** Текущая длительность записи */ onDuration(handler: (seconds: number) => void): void; } interface RecordedAudio { uri: string; // локальный путь к файлу duration: number; // секунды size: number; // байты mimeType: 'audio/mp4'; waveform: number[]; // нормализованные амплитуды для визуализации } // AudioPlayer.ts class AudioPlayer { async play(uri: string): Promise; async pause(): void; async stop(): void; async seekTo(seconds: number): void; onProgress(handler: (current: number, total: number) => void): void; onComplete(handler: () => void): void; } ``` **Формат аудио:** AAC в контейнере M4A. - Нативная поддержка на iOS и Android без дополнительных кодеков - Хорошее сжатие (голос 5 сек ≈ 30-50 КБ при 32 kbps) - Параметры записи: mono, 32 kbps, sample rate 22050 Hz ### 8.5 Сервис MAM (services/xmpp/MAMService.ts) ```typescript class MAMService { /** * Запрос архива сообщений. * При первом подключении — загрузка последних N сообщений для каждого чата. * При повторном подключении — загрузка сообщений с момента последнего известного. */ async fetchHistory(params: { with?: string; // JID собеседника (опционально) before?: string; // ID сообщения для пагинации «назад» after?: string; // ID для пагинации «вперёд» max?: number; // лимит (по умолчанию 50) }): Promise; /** * Запрос сообщений, пришедших пока клиент был офлайн. * Вызывается сразу после успешного connect. */ async syncSinceLastMessage(lastKnownId: string): Promise; } ``` ### 8.6 Сервис Roster (services/xmpp/RosterService.ts) ```typescript class RosterService { /** Получить текущий список контактов */ async getRoster(): Promise; /** Добавить контакт (отправить subscription request) */ async addContact(jid: string, name?: string): Promise; /** Удалить контакт */ async removeContact(jid: string): Promise; /** Принять входящий запрос на подписку */ async acceptSubscription(jid: string): Promise; /** Отклонить входящий запрос */ async denySubscription(jid: string): Promise; /** Обновление ростера (push от сервера) */ onRosterUpdate(handler: (contacts: Contact[]) => void): void; /** Входящий subscription request */ onSubscriptionRequest(handler: (jid: string) => void): void; } ``` ### 8.7 Локальная база данных (services/storage/) SQLite через op-sqlite. Три основные таблицы: ```sql -- Контакты CREATE TABLE contacts ( jid TEXT PRIMARY KEY, name TEXT, avatar_url TEXT, subscription TEXT DEFAULT 'none', -- none | to | from | both updated_at INTEGER NOT NULL ); -- Чаты (список бесед) CREATE TABLE chats ( id TEXT PRIMARY KEY, -- bare JID собеседника или room JID type TEXT NOT NULL, -- 'chat' | 'groupchat' title TEXT, -- имя контакта или название группы last_message_id TEXT, last_message_text TEXT, last_message_at INTEGER, unread_count INTEGER DEFAULT 0, pinned INTEGER DEFAULT 0, updated_at INTEGER NOT NULL ); -- Сообщения CREATE TABLE messages ( id TEXT PRIMARY KEY, -- XMPP stanza id chat_id TEXT NOT NULL, -- ссылка на chats.id from_jid TEXT NOT NULL, to_jid TEXT NOT NULL, type TEXT NOT NULL, -- 'text' | 'voice' body TEXT, -- Голосовое сообщение voice_url TEXT, voice_duration INTEGER, voice_mime_type TEXT, voice_size INTEGER, voice_waveform TEXT, -- JSON array voice_local_uri TEXT, -- кешированный локальный путь -- Статус status TEXT DEFAULT 'sent', -- 'sending' | 'sent' | 'delivered' | 'displayed' | 'error' -- Timestamps created_at INTEGER NOT NULL, server_timestamp INTEGER, -- timestamp из MAM FOREIGN KEY (chat_id) REFERENCES chats(id) ); CREATE INDEX idx_messages_chat_id ON messages(chat_id, created_at); CREATE INDEX idx_messages_status ON messages(status); ``` ### 8.8 Zustand stores #### useAuthStore ```typescript interface AuthState { jid: string | null; password: string | null; // хранится в Keychain/Keystore, не в памяти isAuthenticated: boolean; isConnecting: boolean; connectionError: string | null; login(jid: string, password: string): Promise; register(username: string, password: string): Promise; logout(): Promise; } ``` #### useChatsStore ```typescript interface ChatsState { chats: Chat[]; currentChatId: string | null; messages: Record; // chatId → messages loadChats(): Promise; loadMessages(chatId: string, before?: string): Promise; addMessage(message: Message): void; updateMessageStatus(messageId: string, status: MessageStatus): void; incrementUnread(chatId: string): void; resetUnread(chatId: string): void; setCurrentChat(chatId: string | null): void; } ``` #### useContactsStore ```typescript interface ContactsState { contacts: Contact[]; pendingRequests: string[]; // входящие запросы на добавление loadContacts(): Promise; addContact(jid: string, name?: string): Promise; removeContact(jid: string): Promise; acceptRequest(jid: string): Promise; denyRequest(jid: string): Promise; } ``` #### usePresenceStore ```typescript interface PresenceState { presences: Record; // bare JID → presence setPresence(jid: string, info: PresenceInfo): void; getPresence(jid: string): PresenceInfo; } interface PresenceInfo { status: 'online' | 'away' | 'dnd' | 'offline'; statusText?: string; lastSeen?: number; // timestamp } ``` ### 8.9 Экраны #### LoginScreen / RegisterScreen - Поля: JID (или username при регистрации), пароль - Адрес сервера — конфигурируемый (по умолчанию `jabogram.example.com`) - Валидация ввода - Индикация состояния подключения - Сохранение credentials в Keychain (iOS) / Keystore (Android) #### ChatListScreen - Список бесед, отсортированный по дате последнего сообщения - Каждый элемент: аватар, имя, превью последнего сообщения, время, счётчик непрочитанных - Для голосовых: превью = "🎤 Голосовое сообщение" - Индикатор онлайн-статуса контакта - Pull-to-refresh для синхронизации - FAB-кнопка для создания нового чата #### ChatScreen - Список сообщений (FlatList, inverted) - Пагинация: подгрузка старых сообщений при скролле вверх (MAM) - Текстовые сообщения — пузыри (свои справа, чужие слева) - Голосовые сообщения — специальный виджет с кнопкой Play, waveform, длительностью - Индикатор «печатает…» над полем ввода - Статусы сообщений (✓ отправлено, ✓✓ доставлено, синие ✓✓ прочитано) - Поле ввода текста + кнопка отправки - Кнопка микрофона: нажать и удерживать → запись; отпустить → отправка; свайп влево → отмена - При записи: оверлей с таймером, визуализацией амплитуды, подсказкой «свайп для отмены» #### ContactsScreen - Список контактов с онлайн-статусами - Секция «Входящие запросы» (если есть) — принять / отклонить - Поиск по имени / JID - Кнопка добавления нового контакта #### AddContactScreen - Поле ввода JID - Отправка subscription request #### CreateGroupScreen - Название группы - Выбор участников из контактов - Создание MUC-комнаты #### ProfileScreen - Аватар (выбор из галереи, обрезка) - Никнейм - Статус-текст - Кнопка «Выйти» ### 8.10 Навигация ``` AppNavigator (Stack) ├── AuthStack (когда !isAuthenticated) │ ├── LoginScreen │ └── RegisterScreen │ └── MainStack (когда isAuthenticated) ├── MainTabs (Bottom Tab) │ ├── ChatsTab │ │ └── ChatListScreen │ ├── ContactsTab │ │ └── ContactsScreen │ └── ProfileTab │ └── ProfileScreen │ ├── ChatScreen (modal push) ├── AddContactScreen (modal push) ├── CreateGroupScreen (modal push) └── GroupInfoScreen (modal push) ``` ### 8.11 Компоненты: детальное описание ключевых #### ChatInput ``` ┌──────────────────────────────────────────────────┐ │ ┌──────────────────────────────────┐ ┌──────┐ │ │ │ Введите сообщение... │ │ 🎤 │ │ │ └──────────────────────────────────┘ └──────┘ │ └──────────────────────────────────────────────────┘ ``` Логика переключения: - Если текст пуст → показывает кнопку 🎤 (микрофон) - Если текст не пуст → кнопка заменяется на ➤ (отправить) - Отправка chat state `composing` при наборе, `paused` через 3 сек неактивности #### VoiceRecordOverlay ``` ┌──────────────────────────────────────────────────┐ │ │ │ ◉ 0:05 ← свайп для отмены │ │ ▂▃▅▇▅▃▂▃▅▇▅▃ (визуализация амплитуды) │ │ │ └──────────────────────────────────────────────────┘ ``` Появляется при long press на 🎤. Отпускание пальца → отправка. Свайп влево → отмена. #### VoiceMessageBubble ``` ┌───────────────────────────────┐ │ ▶ ▂▃▅▇▅▃▂▃▅▇▅▃▂ 0:05 │ │ 12:34 ✓✓ │ └───────────────────────────────┘ ``` - Кнопка play/pause - Waveform (из данных `voice_waveform`) - Длительность / текущая позиция при проигрывании - Прогресс-бар поверх waveform - Время отправки и статус доставки ### 8.12 Хуки #### useXMPP ```typescript function useXMPP() { // Автоматическое подключение при наличии credentials // Подписка на события (сообщения, presence, roster updates) // Реконнект при восстановлении сети // CSI: active при foreground, inactive при background } ``` #### useAudioRecorder ```typescript function useAudioRecorder() { return { isRecording: boolean; duration: number; amplitude: number; start: () => Promise; stop: () => Promise; cancel: () => Promise; }; } ``` #### useAudioPlayer ```typescript function useAudioPlayer(messageId: string) { return { isPlaying: boolean; progress: number; // 0..1 currentTime: number; duration: number; play: (uri: string) => Promise; pause: () => void; seekTo: (seconds: number) => void; }; } ``` #### useMessages ```typescript function useMessages(chatId: string) { return { messages: Message[]; isLoading: boolean; hasMore: boolean; loadMore: () => Promise; // подгрузка старых (MAM) sendText: (body: string) => void; sendVoice: (audio: RecordedAudio) => Promise; }; } ``` --- ## 9. Клиентское приложение — iOS ### 9.1 Точка входа ```javascript // react-native-ios/index.js import { AppRegistry } from 'react-native'; import { App } from 'react-native-lib'; import { name as appName } from './app.json'; AppRegistry.registerComponent(appName, () => App); ``` ### 9.2 Info.plist — требуемые разрешения ```xml NSMicrophoneUsageDescription Jabogram needs microphone access to record voice messages NSPhotoLibraryUsageDescription Jabogram needs photo library access to set your avatar NSCameraUsageDescription Jabogram needs camera access to take a photo for your avatar UIBackgroundModes remote-notification fetch ``` ### 9.3 Push-уведомления (APNs) 1. Включить Push Notifications capability в Xcode 2. Настроить `Jabogram.entitlements`: ```xml aps-environment production ``` 3. Зарегистрировать APNs device token через Firebase Cloud Messaging: - `@react-native-firebase/messaging` получает FCM token - FCM-токен регистрируется на XMPP-сервере через XEP-0357 4. При получении push → показать локальное уведомление, если приложение в фоне ### 9.4 Хранение credentials - **iOS Keychain** через `react-native-keychain` - Хранить JID и пароль - Автоматический вход при запуске, если есть сохранённые credentials ### 9.5 Аудио-сессия Настройка AVAudioSession при записи: - Category: `playAndRecord` - Mode: `default` - При воспроизведении: через динамик (не ушной, если телефон не у уха) --- ## 10. Клиентское приложение — Android ### 10.1 Точка входа Аналогично iOS: ```javascript // react-native-android/index.js import { AppRegistry } from 'react-native'; import { App } from 'react-native-lib'; import { name as appName } from './app.json'; AppRegistry.registerComponent(appName, () => App); ``` ### 10.2 AndroidManifest.xml — разрешения ```xml ``` ### 10.3 Push-уведомления (FCM) 1. Файл `google-services.json` в `android/app/` (не в git, добавить в `.gitignore`) 2. `@react-native-firebase/messaging` для получения FCM-токена 3. Регистрация токена на XMPP-сервере (XEP-0357) 4. Notification channels: ```kotlin // В MainActivity или Application val channel = NotificationChannel( "messages", "Messages", NotificationManager.IMPORTANCE_HIGH ) ``` ### 10.4 Хранение credentials - **Android Keystore** через `react-native-keychain` - Тот же API, что и на iOS — библиотека абстрагирует платформу ### 10.5 Специфика аудиозаписи - Runtime permission `RECORD_AUDIO` — запрашивать перед первой записью - Android 13+: дополнительно `POST_NOTIFICATIONS` - Формат записи: AAC (.m4a) — поддерживается нативно через MediaRecorder --- ## 11. Модели данных ### 11.1 TypeScript-типы (react-native-lib/src/types/index.ts) ```typescript /** Статус доставки сообщения */ export type MessageStatus = 'sending' | 'sent' | 'delivered' | 'displayed' | 'error'; /** Тип сообщения */ export type MessageType = 'text' | 'voice'; /** Тип чата */ export type ChatType = 'chat' | 'groupchat'; /** Статус присутствия */ export type PresenceStatus = 'online' | 'away' | 'dnd' | 'offline'; /** Статус подписки ростера */ export type SubscriptionStatus = 'none' | 'to' | 'from' | 'both'; /** Сообщение */ export interface Message { id: string; chatId: string; fromJid: string; toJid: string; type: MessageType; body?: string; // Голосовое сообщение voice?: { url: string; duration: number; // секунды mimeType: string; size: number; // байты waveform: number[]; // нормализованные значения 0..1 localUri?: string; // путь к кешированному файлу }; status: MessageStatus; createdAt: number; // unix timestamp ms serverTimestamp?: number; // timestamp из MAM } /** Чат / беседа */ export interface Chat { id: string; // bare JID или room JID type: ChatType; title: string; avatarUrl?: string; lastMessage?: { id: string; text: string; timestamp: number; }; unreadCount: number; pinned: boolean; } /** Контакт */ export interface Contact { jid: string; // bare JID name?: string; avatarUrl?: string; subscription: SubscriptionStatus; } /** Информация о присутствии */ export interface PresenceInfo { status: PresenceStatus; statusText?: string; lastSeen?: number; // unix timestamp } /** Записанное аудио */ export interface RecordedAudio { uri: string; duration: number; size: number; mimeType: 'audio/mp4'; waveform: number[]; } /** Данные аутентификации */ export interface AuthCredentials { jid: string; password: string; server: string; } ``` --- ## 12. Сценарии взаимодействия ### 12.1 Регистрация нового пользователя ``` Клиент Сервер (ejabberd) │ │ │──── WSS connect ─────────────────────►│ │◄─── stream features (SASL, IBR) ──────│ │ │ │──── IQ: register (XEP-0077) ─────────►│ │ username="alice", password="***" │ │◄─── IQ result: success ───────────────│ │ │ │──── SASL auth (SCRAM-SHA-256) ───────►│ │◄─── success ──────────────────────────│ │ │ │──── bind resource "ios" ─────────────►│ │◄─── bound: alice@server/ios ──────────│ │ │ │──── enable stream mgmt (XEP-0198) ──►│ │◄─── enabled ──────────────────────────│ │ │ │──── enable carbons (XEP-0280) ──────►│ │◄─── result ───────────────────────────│ │ │ │──── initial presence ────────────────►│ │ │ ``` ### 12.2 Вход существующего пользователя ``` Клиент Сервер │ │ │──── WSS connect ─────────────────────►│ │◄─── stream features ─────────────────│ │──── SASL auth ───────────────────────►│ │◄─── success ──────────────────────────│ │──── bind ────────────────────────────►│ │◄─── bound ────────────────────────────│ │──── stream mgmt ─────────────────────►│ │──── enable carbons ──────────────────►│ │──── initial presence ────────────────►│ │ │ │──── MAM: sync since last known ──────►│ // XEP-0313 │◄─── archived messages ───────────────│ │ │ │──── get roster ──────────────────────►│ │◄─── roster items ────────────────────│ │◄─── presence updates ───────────────│ │ │ ``` ### 12.3 Отправка текстового сообщения ``` Alice (клиент) Сервер Bob (клиент) │ │ │ │ 1. UI: ввод текста │ │ │ 2. chatstate=composing │ │ │────────────────────────►│─────────────────────►│ 3. «Alice печатает…» │ │ │ │ 4. Нажатие «отправить» │ │ │ 5. Сохранить в SQLite │ │ │ status='sending' │ │ │ │ │ │ 6. + receipt │ │ │────────────────────────►│ │ │ │ 7. route message │ │ │─────────────────────►│ 8. Получено │ │ │ 9. Сохранить в SQLite │ │ │ 10. Показать в UI │ │ │ │ │◄─────────────────────│ 11. (XEP-0184) │◄────────────────────────│ │ │ 12. status='delivered' │ │ │ │ │ │ │◄─────────────────────│ 13. (XEP-0333) │◄────────────────────────│ │ (при открытии чата) │ 14. status='displayed' │ │ │ │ │ ``` ### 12.4 Отправка голосового сообщения ``` Alice (клиент) Сервер Bob (клиент) │ │ │ │ 1. Long press 🎤 │ │ │ 2. Начало записи │ │ │ ...запись... │ │ │ 3. Отпускание пальца │ │ │ 4. stop() → RecordedAudio │ │ 5. Вычисление waveform │ │ │ │ │ │ 6. IQ: request upload │ │ │ slot (XEP-0363) │ │ │────────────────────────►│ │ │◄────────────────────────│ 7. PUT url + GET url│ │ │ │ │ 8. HTTP PUT (файл) │ │ │────────────────────────►│ 9. Сохранить файл │ │◄──── 201 Created ──────│ │ │ │ │ │ 10. с OOB │ │ │ + │ │ │ + receipt request │ │ │────────────────────────►│ │ │ │─────────────────────►│ 11. Получить сообщение │ │ │ 12. Распознать voice-message │ │ │ 13. Показать VoiceMessageBubble │ │ │ 14. По нажатию Play: │ │ │ GET файл → воспроизвести │ │ │ ``` ### 12.5 Получение офлайн-сообщения (push) ``` Bob (офлайн) Сервер Push Gateway Телефон Bob │ │ │ │ │ │◄── message from Alice │ │ │ │ │ │ │ │ mod_push: │ │ │ │ Bob offline → │ │ │ │ trigger push │ │ │ │─────────────────────►│ │ │ │ │ APNs / FCM │ │ │ │───────────────────►│ │ │ │ │ notification │ │ │ │ │ Bob открывает │ │ │ │ приложение │ │ │ │────── connect ─────────►│ │ │ │────── MAM sync ────────►│ │ │ │◄── пропущенные msg ────│ │ │ │ │ │ │ ``` ### 12.6 Создание группового чата ``` Alice (клиент) Сервер (MUC) │ │ │ 1. IQ: create room │ │ room@conference.jabogram.example.com │────────────────────────────────────── ►│ │◄── room created ─────────────────────│ │ │ │ 2. IQ: configure room │ │ (название, persistent, etc.) │ │──────────────────────────────────────►│ │◄── configured ───────────────────────│ │ │ │ 3. Invite участников │ │──────────────────────────────────────►│ │ ──────────────│──► Bob gets invite │ ──────────────│──► Carol gets invite │ │ ``` ### 12.7 Синхронизация истории при подключении ``` Клиент Сервер │ │ │ 1. Подключение + auth │ │──────────────────────────────────────►│ │ │ │ 2. Прочитать lastKnownMessageId │ │ из локального SQLite │ │ │ │ 3. MAM query: after=lastKnownId │ │──────────────────────────────────────►│ │◄── archived messages (порциями) ─────│ │ │ │ 4. Для каждого нового сообщения: │ │ - сохранить в SQLite │ │ - обновить chat list │ │ - обновить unread counter │ │ │ │ 5. Если lastKnownId отсутствует │ │ (первый вход): загрузить │ │ последние 50 сообщений │ │ │ ``` --- ## 13. Безопасность ### 13.1 Транспортный уровень | Требование | Реализация | |---|---| | Шифрование соединения | TLS 1.2+ обязателен. STARTTLS для XMPP C2S (порт 5222), WSS для WebSocket (порт 5443) | | Сертификаты | Let's Encrypt или корпоративный CA | | Certificate pinning | Опционально для production (через react-native-ssl-pinning) | ### 13.2 Аутентификация | Требование | Реализация | |---|---| | SASL-механизм | SCRAM-SHA-256 (предпочтительно), SCRAM-SHA-1 (fallback). PLAIN запрещён | | Хранение пароля на сервере | scram-формат (hash + salt + iterations). Никогда plain | | Хранение пароля на клиенте | iOS Keychain / Android Keystore через react-native-keychain | | Регистрация | XEP-0077 с rate limiting (ip_access в ejabberd) | ### 13.3 Загрузка файлов | Требование | Реализация | |---|---| | Авторизация upload | Только аутентифицированные пользователи получают upload slot | | Ограничение размера | max_size: 10 MB | | Допустимые типы | Только audio/mp4, audio/ogg (проверка на клиенте и Content-Type при upload) | | URL upload-слотов | Случайные, непредсказуемые пути | ### 13.4 Защита от злоупотреблений - Rate limiting на уровне ejabberd (shaper) - Rate limiting на уровне nginx - mod_blocking для блокировки пользователей - Ограничение частоты регистраций с одного IP --- ## 14. Развёртывание ### 14.1 Серверная инфраструктура **Минимальные требования:** - VPS: 2 vCPU, 4 GB RAM, 40 GB SSD - Ubuntu 22.04+ или Debian 12+ - Docker Engine 24+ - Домен с DNS A-записью **Порядок развёртывания:** 1. Подготовить сервер: ```bash apt update && apt install -y docker.io docker-compose-plugin ``` 2. Получить TLS-сертификат: ```bash apt install -y certbot certbot certonly --standalone -d jabogram.example.com cp /etc/letsencrypt/live/jabogram.example.com/fullchain.pem server/ejabberd/certs/ cp /etc/letsencrypt/live/jabogram.example.com/privkey.pem server/ejabberd/certs/ ``` 3. Создать `.env`: ``` POSTGRES_PASSWORD= ``` 4. Запустить: ```bash cd server && docker compose up -d ``` 5. Проверить: ```bash docker compose exec ejabberd ejabberdctl status ``` ### 14.2 Сборка клиентских приложений **iOS:** ```bash cd react-native-ios npm install cd ios && pod install && cd .. npx react-native run-ios --mode Release # или: xcodebuild -workspace ios/Jabogram.xcworkspace -scheme Jabogram -configuration Release ``` **Android:** ```bash cd react-native-android npm install cd android && ./gradlew assembleRelease # APK: android/app/build/outputs/apk/release/app-release.apk ``` ### 14.3 Переменные окружения / конфигурация клиента Файл `react-native-lib/src/config.ts`: ```typescript export const config = { /** XMPP домен */ domain: 'jabogram.example.com', /** WebSocket URL */ wsUrl: 'wss://jabogram.example.com/ws', /** Максимальная длительность голосового сообщения (секунды) */ maxVoiceDuration: 300, /** Максимальный размер файла (байты) */ maxFileSize: 10 * 1024 * 1024, /** MAM: размер страницы при загрузке истории */ mamPageSize: 50, /** Аудио: битрейт AAC */ audioBitrate: 32000, /** Аудио: sample rate */ audioSampleRate: 22050, /** Количество точек waveform */ waveformPoints: 40, }; ``` --- ## 15. Тестирование ### 15.1 Unit-тесты (react-native-lib) | Модуль | Что тестировать | |---|---| | `jid.ts` | Парсинг/форматирование JID | | `MessageService` | Формирование XML stanza (text, voice, receipts, markers) | | `FileUploadService` | Парсинг upload slot response | | `MAMService` | Парсинг archived messages | | `AudioRecorder` | Вычисление waveform из амплитуд | | Zustand stores | Все мутации состояния | | `MessageRepository` | CRUD операции с SQLite | | Компоненты | Snapshot-тесты ключевых компонентов | Инструменты: **Jest** + **React Native Testing Library** ### 15.2 Интеграционные тесты | Сценарий | Описание | |---|---| | Регистрация → вход | Клиент регистрирует аккаунт, отключается, подключается заново | | Обмен текстом | Два клиента обмениваются сообщениями, проверка доставки | | Голосовое сообщение | Upload → отправка → скачивание → проверка файла | | Офлайн-доставка | Отправка сообщения офлайн-пользователю → подключение → MAM sync | | Групповой чат | Создание, приглашение, обмен сообщениями | Инструменты: **Detox** (E2E для React Native) + тестовый ejabberd в Docker ### 15.3 Серверные тесты - Проверка конфигурации ejabberd при старте - Healthcheck PostgreSQL - Проверка доступности WebSocket endpoint - Проверка работы HTTP File Upload --- ## 16. Ограничения и допущения v1 1. **Один сервер** — без федерации (S2S отключён), все пользователи на одном домене 2. **Нет E2E-шифрования** — только transport encryption (TLS). OMEMO — в будущих версиях 3. **Голос только как файлы** — нет VoIP / WebRTC 4. **Нет произвольных файлов** — только голосовые сообщения (audio/mp4) 5. **Нет web-клиента** — только iOS и Android 6. **Нет редактирования/удаления сообщений** — в будущих версиях (XEP-0308, XEP-0424) 7. **Аватар** — только из галереи, без камеры в v1 8. **Поиск пользователей** — только по точному JID, нет глобального каталога 9. **Push-уведомления** — через Firebase (FCM для обеих платформ, APNs через FCM) 10. **Максимум 200 участников** в групповом чате