initial
This commit is contained in:
commit
f854c14c45
34
README.md
Normal file
34
README.md
Normal file
@ -0,0 +1,34 @@
|
||||
# Jabogram
|
||||
|
||||
XMPP-мессенджер для обмена текстовыми и голосовыми сообщениями.
|
||||
|
||||
## Возможности
|
||||
|
||||
- Личные и групповые чаты
|
||||
- Текстовые сообщения с подтверждением доставки и прочтения
|
||||
- Голосовые сообщения (запись, отправка, воспроизведение с визуализацией)
|
||||
- Индикатор набора текста
|
||||
- Онлайн-статусы контактов
|
||||
- Синхронизация истории сообщений
|
||||
- Push-уведомления (APNs / FCM)
|
||||
|
||||
## Стек
|
||||
|
||||
- **Сервер:** ejabberd + PostgreSQL + Nginx (Docker)
|
||||
- **Клиент:** React Native + TypeScript + stanza.js + Zustand
|
||||
|
||||
## Структура проекта
|
||||
|
||||
```
|
||||
jabogram/
|
||||
├── doc/ # Документация
|
||||
│ └── DESIGN.md # Дизайн-документ
|
||||
├── server/ # Серверная часть (Docker, конфиги ejabberd)
|
||||
├── react-native-lib/ # Общий код клиентов (компоненты, сервисы, stores)
|
||||
├── react-native-ios/ # iOS-приложение
|
||||
└── react-native-android/ # Android-приложение
|
||||
```
|
||||
|
||||
## Документация
|
||||
|
||||
- [Дизайн-документ](doc/DESIGN.md) — полное техническое описание архитектуры, протокола, моделей данных, сценариев и развёртывания.
|
||||
1849
doc/DESIGN.md
Normal file
1849
doc/DESIGN.md
Normal file
File diff suppressed because it is too large
Load Diff
1
server/.gitignore
vendored
Normal file
1
server/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
secrets.env
|
||||
194
server/README.md
Normal file
194
server/README.md
Normal file
@ -0,0 +1,194 @@
|
||||
# Snikket Server
|
||||
|
||||
XMPP-сервер на базе [Snikket](https://snikket.org/) (обёртка над Prosody с предварительной конфигурацией).
|
||||
|
||||
## Состав сервисов
|
||||
|
||||
| Сервис | Образ | Назначение |
|
||||
|--------|-------|------------|
|
||||
| `snikket_server` | `snikket/snikket-server:stable` | Ядро — Prosody XMPP-сервер |
|
||||
| `snikket_proxy` | `snikket/snikket-web-proxy:stable` | Веб-прокси (nginx) |
|
||||
| `snikket_certs` | `snikket/snikket-cert-manager:stable` | Автоматическое получение TLS-сертификатов (Let's Encrypt) |
|
||||
| `snikket_portal` | `snikket/snikket-web-portal:stable` | Веб-портал для управления пользователями и инвайтами |
|
||||
| `s3_upload_handler` | собирается из `./s3-upload-handler` | Обработчик загрузки файлов — принимает файлы от XMPP-клиентов и сохраняет в S3 |
|
||||
| `postgres` | `postgres:15` | База данных PostgreSQL для Prosody |
|
||||
|
||||
Все сервисы работают в режиме `network_mode: host`.
|
||||
|
||||
## Требования
|
||||
|
||||
### DNS-записи
|
||||
|
||||
Для домена `chat.example.org` необходимо создать 3 записи:
|
||||
|
||||
| Тип | Имя | Значение |
|
||||
|-----|-----|----------|
|
||||
| A | `chat.example.org` | IP-адрес сервера |
|
||||
| CNAME | `groups.chat.example.org` | `chat.example.org` |
|
||||
| CNAME | `share.chat.example.org` | `chat.example.org` |
|
||||
|
||||
Опционально: AAAA-запись для IPv6, SRV-запись `_xmpps-client._tcp` для подключения через порт 443.
|
||||
|
||||
### Порты
|
||||
|
||||
| Порт | Протокол | Назначение |
|
||||
|------|----------|------------|
|
||||
| 80 | TCP | HTTP (ACME challenges, редирект на HTTPS) |
|
||||
| 443 | TCP | HTTPS (веб-портал, file sharing) |
|
||||
| 5222 | TCP | XMPP client-to-server |
|
||||
| 5269 | TCP | XMPP server-to-server (федерация) |
|
||||
| 5000 | TCP | Proxy65 (передача файлов) |
|
||||
| 5050 | TCP | S3 upload handler (HTTP Upload External) |
|
||||
| 5432 | TCP | PostgreSQL |
|
||||
| 3478, 3479 | TCP+UDP | STUN/TURN |
|
||||
| 5349, 5350 | TCP+UDP | STUN/TURN over TLS |
|
||||
| 49152–65535 | UDP | TURN relay (аудио/видео данные) |
|
||||
|
||||
## Конфигурация
|
||||
|
||||
### prosody.cfg.lua
|
||||
|
||||
Кастомная конфигурация Prosody, монтируется в контейнер `snikket_server` как `/etc/prosody/conf.d/custom.cfg.lua`. Содержит:
|
||||
|
||||
- **PostgreSQL** — `storage = "sql"` с драйвером PostgreSQL (подключение к `127.0.0.1:5432`)
|
||||
- **HTTP Upload External** — `mod_http_upload_external` для загрузки файлов через внешний S3 upload handler
|
||||
|
||||
### secrets.env
|
||||
|
||||
Файл с секретами, **не коммитится в git** (добавлен в `.gitignore`). Перед первым запуском необходимо создать его и заполнить реальными значениями:
|
||||
|
||||
```bash
|
||||
cp secrets.env.example secrets.env
|
||||
# отредактировать secrets.env
|
||||
```
|
||||
|
||||
| Переменная | Описание |
|
||||
|------------|----------|
|
||||
| `UPLOAD_SECRET` | Shared secret (должен совпадать с `http_upload_external_secret` в `prosody.cfg.lua`) |
|
||||
| `AWS_ACCESS_KEY_ID` | Ключ доступа AWS/S3 |
|
||||
| `AWS_SECRET_ACCESS_KEY` | Секретный ключ AWS/S3 |
|
||||
| `POSTGRES_PASSWORD` | Пароль PostgreSQL |
|
||||
|
||||
### S3 Upload Handler (environment)
|
||||
|
||||
Несекретные параметры задаются в `docker-compose.yml` в секции `environment` сервиса `s3_upload_handler`:
|
||||
|
||||
| Переменная | Описание |
|
||||
|------------|----------|
|
||||
| `S3_BUCKET` | Имя S3-бакета |
|
||||
| `S3_REGION` | Регион S3 |
|
||||
| `S3_ENDPOINT` | URL S3-совместимого хранилища (MinIO и др.), не задавать для AWS |
|
||||
| `PRESIGN_EXPIRE` | Время жизни presigned URL для скачивания (секунды, по умолчанию `3600`) |
|
||||
|
||||
### snikket.conf
|
||||
|
||||
### Обязательные параметры
|
||||
|
||||
| Переменная | Описание |
|
||||
|------------|----------|
|
||||
| `SNIKKET_DOMAIN` | Домен сервера. **Нельзя изменить после первого запуска.** |
|
||||
| `SNIKKET_ADMIN_EMAIL` | Email администратора (используется для Let's Encrypt) |
|
||||
|
||||
### Основные параметры
|
||||
|
||||
| Переменная | По умолчанию | Описание |
|
||||
|------------|-------------|----------|
|
||||
| `SNIKKET_SITE_NAME` | значение `SNIKKET_DOMAIN` | Человекочитаемое имя сервера |
|
||||
| `SNIKKET_RETENTION_DAYS` | `7` | Сколько дней хранить сообщения (MAM) и загруженные файлы |
|
||||
| `SNIKKET_UPLOAD_STORAGE_GB` | без лимита | Лимит дискового пространства для загруженных файлов (напр. `1.5`) |
|
||||
| `SNIKKET_LOGLEVEL` | `info` | Уровень логирования: `error`, `warn`, `info`, `debug` |
|
||||
| `SNIKKET_ABUSE_EMAIL` | — | Публичный email для жалоб |
|
||||
| `SNIKKET_SECURITY_EMAIL` | — | Публичный email для сообщений о безопасности |
|
||||
| `SNIKKET_UPDATE_CHECK` | `1` | Установить `0` чтобы отключить проверку обновлений |
|
||||
|
||||
### Параметры reverse proxy
|
||||
|
||||
| Переменная | По умолчанию | Описание |
|
||||
|------------|-------------|----------|
|
||||
| `SNIKKET_TWEAK_HTTP_PORT` | `80` | HTTP-порт (изменить при работе за reverse proxy) |
|
||||
| `SNIKKET_TWEAK_HTTPS_PORT` | `443` | HTTPS-порт (изменить при работе за reverse proxy) |
|
||||
|
||||
### Параметры TURN-сервера
|
||||
|
||||
| Переменная | По умолчанию | Описание |
|
||||
|------------|-------------|----------|
|
||||
| `SNIKKET_TWEAK_TURNSERVER` | `1` | Установить `0` для отключения встроенного TURN |
|
||||
| `SNIKKET_TWEAK_TURNSERVER_DOMAIN` | значение `SNIKKET_DOMAIN` | Домен внешнего TURN-сервера |
|
||||
| `SNIKKET_TWEAK_TURNSERVER_PORT` | `3478` | Порт TURN-сервера |
|
||||
| `SNIKKET_TWEAK_TURNSERVER_SECRET` | автогенерация | Shared secret для аутентификации TURN |
|
||||
|
||||
### Прочие параметры
|
||||
|
||||
| Переменная | По умолчанию | Описание |
|
||||
|------------|-------------|----------|
|
||||
| `SNIKKET_TWEAK_IPV6` | `1` | Установить `0` для отключения IPv6 |
|
||||
| `SNIKKET_TWEAK_STORAGE` | `files` | Бэкенд хранения: `files` или `sqlite` (превью) |
|
||||
| `SNIKKET_TWEAK_EXTRA_CONFIG` | — | Путь/glob к дополнительным файлам конфигурации Prosody |
|
||||
| `SNIKKET_PROXY65_PORT` | `5000` | Порт proxy для передачи файлов |
|
||||
| `SNIKKET_TWEAK_DNSSEC` | — | Включить поддержку DNSSEC и DANE |
|
||||
|
||||
## Запуск
|
||||
|
||||
```bash
|
||||
cd server
|
||||
|
||||
# 1. Настроить конфигурацию
|
||||
# отредактировать snikket.conf значениями вашего домена
|
||||
|
||||
# 2. Заполнить секреты
|
||||
cp secrets.env.example secrets.env
|
||||
# отредактировать secrets.env реальными значениями
|
||||
|
||||
# 3. Запустить
|
||||
docker compose up -d
|
||||
|
||||
# 3. Создать admin-аккаунт
|
||||
docker exec snikket create-invite --admin --group default
|
||||
```
|
||||
|
||||
Инвайт-ссылку открыть в браузере для регистрации администратора.
|
||||
|
||||
## Обновление
|
||||
|
||||
```bash
|
||||
cd server
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Хранение данных
|
||||
|
||||
Данные хранятся в Docker volumes:
|
||||
|
||||
| Volume | Содержимое |
|
||||
|--------|-----------|
|
||||
| `snikket_data` | Данные сервера: аккаунты, сообщения, TLS-сертификаты |
|
||||
| `acme_challenges` | Временные файлы ACME (Let's Encrypt) |
|
||||
| `postgres_data` | Данные PostgreSQL |
|
||||
|
||||
Загруженные файлы хранятся в S3-бакете (не в volumes).
|
||||
|
||||
### Бэкап
|
||||
|
||||
Необходимо сохранить volume `snikket_data` и базу PostgreSQL:
|
||||
|
||||
```bash
|
||||
# Snikket data
|
||||
docker run --rm -v jabogram_snikket_data:/data -v $(pwd):/backup alpine \
|
||||
tar czf /backup/snikket-backup.tar.gz -C /data .
|
||||
|
||||
# PostgreSQL
|
||||
docker exec snikket-postgres pg_dump -U snikket snikket | gzip > snikket-pg-backup.sql.gz
|
||||
```
|
||||
|
||||
## Хранение файлов (S3)
|
||||
|
||||
Загруженные файлы (HTTP File Upload, XEP-0363) хранятся в **S3-бакете** через кастомный `s3_upload_handler`.
|
||||
|
||||
Схема работы:
|
||||
|
||||
1. XMPP-клиент запрашивает у Prosody слот для загрузки файла
|
||||
2. Prosody (`mod_http_upload_external`) генерирует подписанный URL, указывающий на `s3_upload_handler`
|
||||
3. Клиент загружает файл по этому URL
|
||||
4. `s3_upload_handler` проверяет HMAC-подпись и сохраняет файл в S3
|
||||
5. При скачивании — handler генерирует presigned S3 URL и делает redirect
|
||||
80
server/docker-compose.yml
Normal file
80
server/docker-compose.yml
Normal file
@ -0,0 +1,80 @@
|
||||
version: "3.3"
|
||||
|
||||
services:
|
||||
snikket_proxy:
|
||||
container_name: snikket-proxy
|
||||
image: snikket/snikket-web-proxy:stable
|
||||
env_file: snikket.conf
|
||||
network_mode: host
|
||||
volumes:
|
||||
- snikket_data:/snikket
|
||||
- acme_challenges:/var/www/html/.well-known/acme-challenge
|
||||
restart: "unless-stopped"
|
||||
depends_on:
|
||||
- snikket_server
|
||||
|
||||
snikket_certs:
|
||||
container_name: snikket-certs
|
||||
image: snikket/snikket-cert-manager:stable
|
||||
env_file: snikket.conf
|
||||
network_mode: host
|
||||
volumes:
|
||||
- snikket_data:/snikket
|
||||
- acme_challenges:/var/www/.well-known/acme-challenge
|
||||
restart: "unless-stopped"
|
||||
|
||||
snikket_portal:
|
||||
container_name: snikket-portal
|
||||
image: snikket/snikket-web-portal:stable
|
||||
env_file: snikket.conf
|
||||
network_mode: host
|
||||
restart: "unless-stopped"
|
||||
depends_on:
|
||||
- snikket_server
|
||||
|
||||
snikket_server:
|
||||
container_name: snikket
|
||||
image: snikket/snikket-server:stable
|
||||
env_file:
|
||||
- snikket.conf
|
||||
- secrets.env
|
||||
network_mode: host
|
||||
volumes:
|
||||
- snikket_data:/snikket
|
||||
- ./prosody.cfg.lua:/etc/prosody/conf.d/custom.cfg.lua:ro
|
||||
restart: "unless-stopped"
|
||||
depends_on:
|
||||
- postgres
|
||||
- s3_upload_handler
|
||||
|
||||
s3_upload_handler:
|
||||
container_name: snikket-s3-upload
|
||||
build: ./s3-upload-handler
|
||||
network_mode: host
|
||||
env_file:
|
||||
- secrets.env
|
||||
environment:
|
||||
S3_BUCKET: "your-snikket-uploads-bucket"
|
||||
S3_REGION: "us-east-1"
|
||||
# For MinIO or other S3-compatible storage, uncomment:
|
||||
# S3_ENDPOINT: "http://127.0.0.1:9000"
|
||||
PRESIGN_EXPIRE: "3600"
|
||||
restart: "unless-stopped"
|
||||
|
||||
postgres:
|
||||
container_name: snikket-postgres
|
||||
image: postgres:15
|
||||
env_file:
|
||||
- secrets.env
|
||||
environment:
|
||||
POSTGRES_DB: snikket
|
||||
POSTGRES_USER: snikket
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
network_mode: host
|
||||
restart: "unless-stopped"
|
||||
|
||||
volumes:
|
||||
acme_challenges:
|
||||
snikket_data:
|
||||
postgres_data:
|
||||
45
server/prosody.cfg.lua
Normal file
45
server/prosody.cfg.lua
Normal file
@ -0,0 +1,45 @@
|
||||
-- Custom Prosody configuration for Snikket
|
||||
-- This file is included alongside the auto-generated Snikket config.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- PostgreSQL storage
|
||||
----------------------------------------------------------------------
|
||||
storage = "sql"
|
||||
|
||||
sql = {
|
||||
driver = "PostgreSQL";
|
||||
database = "snikket";
|
||||
host = "127.0.0.1";
|
||||
port = 5432;
|
||||
username = "snikket";
|
||||
password = os.getenv("POSTGRES_PASSWORD");
|
||||
}
|
||||
|
||||
----------------------------------------------------------------------
|
||||
-- S3 HTTP Upload (via mod_http_upload_external)
|
||||
----------------------------------------------------------------------
|
||||
|
||||
-- Disable built-in upload module, enable external upload
|
||||
modules_disabled = {
|
||||
"http_upload";
|
||||
}
|
||||
|
||||
modules_enabled = {
|
||||
"http_upload_external";
|
||||
}
|
||||
|
||||
-- URL of the external upload service that handles S3 interaction.
|
||||
-- This is NOT the S3 bucket URL directly. You need to run a separate
|
||||
-- upload handler service (e.g. prosody-filer, or a custom Lambda/endpoint)
|
||||
-- that validates Prosody's HMAC signature and proxies files to/from S3.
|
||||
http_upload_external_base_url = "https://uploads.chat.example.org/upload/"
|
||||
|
||||
-- Shared secret between Prosody and the upload handler service
|
||||
-- (must match the secret configured in the upload handler)
|
||||
http_upload_external_secret = os.getenv("UPLOAD_SECRET")
|
||||
|
||||
-- How long (seconds) the upload URL remains valid
|
||||
http_upload_external_expire_after = 3600
|
||||
|
||||
-- Max file size in bytes (10 MB)
|
||||
http_upload_external_file_size_limit = 10485760
|
||||
11
server/s3-upload-handler/Dockerfile
Normal file
11
server/s3-upload-handler/Dockerfile
Normal file
@ -0,0 +1,11 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY main.py .
|
||||
|
||||
EXPOSE 5050
|
||||
|
||||
CMD ["gunicorn", "-b", "0.0.0.0:5050", "-w", "2", "--timeout", "120", "main:app"]
|
||||
93
server/s3-upload-handler/main.py
Normal file
93
server/s3-upload-handler/main.py
Normal file
@ -0,0 +1,93 @@
|
||||
"""
|
||||
Prosody mod_http_upload_external -> S3 handler.
|
||||
|
||||
Implements the same HMAC protocol as prosody-filer but stores files in S3.
|
||||
|
||||
Flow:
|
||||
1. XMPP client asks Prosody for an upload slot
|
||||
2. Prosody generates a signed PUT URL pointing here
|
||||
3. Client PUTs the file; this handler verifies the HMAC and uploads to S3
|
||||
4. On GET, handler generates a presigned S3 URL and redirects
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import hmac as hmac_mod
|
||||
import logging
|
||||
import os
|
||||
|
||||
import boto3
|
||||
from flask import Flask, Response, redirect, request
|
||||
|
||||
app = Flask(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
log = logging.getLogger("s3-upload-handler")
|
||||
|
||||
SECRET = os.environ["UPLOAD_SECRET"]
|
||||
S3_BUCKET = os.environ["S3_BUCKET"]
|
||||
S3_REGION = os.environ.get("S3_REGION", "us-east-1")
|
||||
S3_ENDPOINT = os.environ.get("S3_ENDPOINT")
|
||||
PRESIGN_EXPIRE = int(os.environ.get("PRESIGN_EXPIRE", "3600"))
|
||||
|
||||
s3_kwargs = {
|
||||
"region_name": S3_REGION,
|
||||
}
|
||||
if S3_ENDPOINT:
|
||||
s3_kwargs["endpoint_url"] = S3_ENDPOINT
|
||||
|
||||
s3 = boto3.client("s3", **s3_kwargs)
|
||||
|
||||
|
||||
def verify_token(path: str, size: str, content_type: str, token: str) -> bool:
|
||||
"""Verify HMAC-SHA256 token from Prosody mod_http_upload_external."""
|
||||
message = f"{path} {size} {content_type}"
|
||||
expected = hmac_mod.new(
|
||||
SECRET.encode(), message.encode(), hashlib.sha256
|
||||
).hexdigest()
|
||||
return hmac_mod.compare_digest(expected, token)
|
||||
|
||||
|
||||
@app.route("/upload/<path:file_path>", methods=["PUT"])
|
||||
def upload(file_path):
|
||||
token = request.args.get("v", "")
|
||||
content_type = request.args.get("t", "application/octet-stream")
|
||||
file_size = request.args.get("s", "0")
|
||||
|
||||
hmac_path = f"upload/{file_path}"
|
||||
|
||||
if not verify_token(hmac_path, file_size, content_type, token):
|
||||
log.warning("HMAC verification failed for %s", hmac_path)
|
||||
return Response("Forbidden", status=403)
|
||||
|
||||
body = request.get_data()
|
||||
|
||||
if len(body) != int(file_size):
|
||||
return Response("Content length mismatch", status=400)
|
||||
|
||||
s3.put_object(
|
||||
Bucket=S3_BUCKET,
|
||||
Key=file_path,
|
||||
Body=body,
|
||||
ContentType=content_type,
|
||||
)
|
||||
|
||||
log.info("Uploaded %s (%s bytes) to s3://%s/%s", file_path, file_size, S3_BUCKET, file_path)
|
||||
return Response(status=201)
|
||||
|
||||
|
||||
@app.route("/upload/<path:file_path>", methods=["GET", "HEAD"])
|
||||
def download(file_path):
|
||||
try:
|
||||
url = s3.generate_presigned_url(
|
||||
"get_object",
|
||||
Params={"Bucket": S3_BUCKET, "Key": file_path},
|
||||
ExpiresIn=PRESIGN_EXPIRE,
|
||||
)
|
||||
except Exception:
|
||||
log.exception("Failed to generate presigned URL for %s", file_path)
|
||||
return Response("Not found", status=404)
|
||||
|
||||
return redirect(url, code=302)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="0.0.0.0", port=5050)
|
||||
3
server/s3-upload-handler/requirements.txt
Normal file
3
server/s3-upload-handler/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
||||
flask==3.1.*
|
||||
gunicorn==23.*
|
||||
boto3==1.36.*
|
||||
7
server/secrets.env.example
Normal file
7
server/secrets.env.example
Normal file
@ -0,0 +1,7 @@
|
||||
# S3 Upload Handler secrets
|
||||
UPLOAD_SECRET=change-me
|
||||
AWS_ACCESS_KEY_ID=change-me
|
||||
AWS_SECRET_ACCESS_KEY=change-me
|
||||
|
||||
# PostgreSQL secrets
|
||||
POSTGRES_PASSWORD=change-me
|
||||
12
server/snikket.conf
Normal file
12
server/snikket.conf
Normal file
@ -0,0 +1,12 @@
|
||||
# Snikket Server Configuration
|
||||
# Copy this file and fill in your values before deploying.
|
||||
|
||||
# Your XMPP domain (e.g., chat.example.org)
|
||||
SNIKKET_DOMAIN=chat.example.org
|
||||
|
||||
# Admin email — used for Let's Encrypt certificate registration
|
||||
SNIKKET_ADMIN_EMAIL=admin@example.org
|
||||
|
||||
# Uncomment the lines below if running behind a reverse proxy (e.g., nginx):
|
||||
# SNIKKET_TWEAK_HTTP_PORT=5080
|
||||
# SNIKKET_TWEAK_HTTPS_PORT=5443
|
||||
297
xmpp_messenger_design_document.md
Normal file
297
xmpp_messenger_design_document.md
Normal file
@ -0,0 +1,297 @@
|
||||
# XMPP Messenger (Text + Voice Messages)
|
||||
|
||||
## 1. Цель документа
|
||||
|
||||
Этот документ предназначен как **техническое задание и дизайн‑документ** для разработки XMPP‑мессенджера с поддержкой:
|
||||
- текстовых сообщений
|
||||
- голосовых сообщений (аудиофайлы, без стриминга)
|
||||
- end‑to‑end шифрования
|
||||
- iOS и Android клиентов на React Native
|
||||
|
||||
Документ рассчитан на использование **Claude Code** как основного помощника в реализации. Поэтому архитектура и формулировки сделаны максимально явными, модульными и детализированными.
|
||||
|
||||
---
|
||||
|
||||
## 2. Область и ограничения
|
||||
|
||||
### Входит в scope
|
||||
- One‑to‑one чаты
|
||||
- Текстовые сообщения
|
||||
- Голосовые сообщения как файлы
|
||||
- E2EE (OMEMO)
|
||||
- Собственная XMPP‑инфраструктура
|
||||
|
||||
### Не входит в scope
|
||||
- Голосовые/видео звонки
|
||||
- Групповые чаты (MUC)
|
||||
- Push‑нотификации (опционально позже)
|
||||
- Федерация между серверами (пока один домен)
|
||||
|
||||
---
|
||||
|
||||
## 3. Общая архитектура
|
||||
|
||||
```
|
||||
Clients (iOS / Android)
|
||||
|
|
||||
| XMPP over WebSocket
|
||||
|
|
||||
XMPP Server (Prosody)
|
||||
|
|
||||
| HTTP Upload
|
||||
|
|
||||
File Storage (local / S3)
|
||||
```
|
||||
|
||||
- XMPP используется **только для сигналинга и сообщений**
|
||||
- Голосовые сообщения передаются как **зашифрованные файлы** через HTTP Upload
|
||||
- Сервер не имеет доступа к содержимому сообщений
|
||||
|
||||
---
|
||||
|
||||
## 4. Структура репозитория
|
||||
|
||||
```
|
||||
./README.md — описание проекта
|
||||
|
||||
./doc/
|
||||
design.md — этот документ
|
||||
|
||||
./server/
|
||||
prosody/
|
||||
prosody.cfg.lua — конфигурация сервера
|
||||
modules/ — кастомные модули (если нужны)
|
||||
deploy/
|
||||
docker-compose.yml — развертывание
|
||||
init.sh — инициализация
|
||||
|
||||
./react-native-lib/
|
||||
src/
|
||||
xmpp/ — XMPP логика
|
||||
crypto/ — OMEMO / шифрование
|
||||
audio/ — запись и воспроизведение
|
||||
storage/ — работа с файлами
|
||||
ui/ — общие UI компоненты
|
||||
types/ — общие типы
|
||||
|
||||
./react-native-ios/
|
||||
ios/ — iOS проект
|
||||
App.tsx — точка входа
|
||||
|
||||
./react-native-android/
|
||||
android/ — Android проект
|
||||
App.tsx — точка входа
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Серверная часть
|
||||
|
||||
### 5.1 Выбор сервера
|
||||
|
||||
- **Prosody** (Lua)
|
||||
- Причины:
|
||||
- простой
|
||||
- модульный
|
||||
- хорошо документирован
|
||||
- легко дорабатывается
|
||||
|
||||
### 5.2 Основные XEP
|
||||
|
||||
| XEP | Назначение |
|
||||
|----|-----------|
|
||||
| XEP-0030 | Service Discovery |
|
||||
| XEP-0198 | Stream Management |
|
||||
| XEP-0363 | HTTP File Upload |
|
||||
| XEP-0384 | OMEMO |
|
||||
| XEP-0313 | MAM |
|
||||
| XEP-0280 | Message Carbons |
|
||||
|
||||
### 5.3 HTTP File Upload
|
||||
|
||||
- Хранение файлов:
|
||||
- локально (MVP)
|
||||
- либо S3‑совместимое хранилище
|
||||
|
||||
- Ограничения:
|
||||
- max размер: 10 MB
|
||||
- max длительность: 5 минут
|
||||
|
||||
### 5.4 Безопасность
|
||||
|
||||
- TLS обязателен
|
||||
- Сервер **не логирует содержимое сообщений**
|
||||
- Логи — только технические (connect/disconnect)
|
||||
|
||||
---
|
||||
|
||||
## 6. Клиентская часть (общая)
|
||||
|
||||
### 6.1 Технологии
|
||||
|
||||
- React Native
|
||||
- TypeScript
|
||||
- xmpp.js
|
||||
|
||||
### 6.2 Архитектура клиента
|
||||
|
||||
```
|
||||
UI
|
||||
↓
|
||||
Chat Logic
|
||||
↓
|
||||
XMPP Service
|
||||
↓
|
||||
Crypto Layer (OMEMO)
|
||||
```
|
||||
|
||||
Каждый слой изолирован.
|
||||
|
||||
---
|
||||
|
||||
## 7. Текстовые сообщения
|
||||
|
||||
### Flow
|
||||
|
||||
1. Пользователь вводит текст
|
||||
2. Текст шифруется OMEMO
|
||||
3. Отправляется XMPP <message/>
|
||||
4. Получатель расшифровывает
|
||||
|
||||
### Формат
|
||||
|
||||
```xml
|
||||
<message to="jid" type="chat">
|
||||
<encrypted xmlns="eu.siacs.conversations.axolotl">
|
||||
...
|
||||
</encrypted>
|
||||
</message>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Голосовые сообщения
|
||||
|
||||
### 8.1 Аудиоформат
|
||||
|
||||
- Кодек: **Opus**
|
||||
- Контейнер:
|
||||
- Android: OGG / WebM
|
||||
- iOS: AAC (fallback)
|
||||
|
||||
### 8.2 Flow голосового сообщения
|
||||
|
||||
1. Запись аудио
|
||||
2. Кодирование
|
||||
3. Шифрование файла
|
||||
4. Upload через XEP‑0363
|
||||
5. Отправка XMPP сообщения со ссылкой
|
||||
|
||||
### 8.3 XMPP сообщение
|
||||
|
||||
```xml
|
||||
<message>
|
||||
<body>Voice message</body>
|
||||
<reference xmlns="urn:xmpp:reference:0" type="data" uri="https://upload/...">
|
||||
<media-sharing xmlns="urn:xmpp:sims:1">
|
||||
<file>
|
||||
<media-type>audio/ogg</media-type>
|
||||
<size>12345</size>
|
||||
<duration>12</duration>
|
||||
</file>
|
||||
</media-sharing>
|
||||
</reference>
|
||||
</message>
|
||||
```
|
||||
|
||||
### 8.4 UX требования
|
||||
|
||||
- запись по удержанию кнопки
|
||||
- отмена свайпом
|
||||
- отображение длительности
|
||||
- ручное воспроизведение
|
||||
|
||||
---
|
||||
|
||||
## 9. Шифрование (OMEMO)
|
||||
|
||||
### Требования
|
||||
|
||||
- Использовать готовую библиотеку
|
||||
- Один identity key на устройство
|
||||
- Поддержка multi‑device
|
||||
|
||||
### Поток
|
||||
|
||||
- Файл шифруется симметричным ключом
|
||||
- Ключ передается через OMEMO
|
||||
|
||||
---
|
||||
|
||||
## 10. iOS‑специфика
|
||||
|
||||
- Background mode: audio
|
||||
- Permissions:
|
||||
- microphone
|
||||
- network
|
||||
|
||||
- Использовать native modules для записи
|
||||
|
||||
---
|
||||
|
||||
## 11. Android‑специфика
|
||||
|
||||
- Foreground service при записи
|
||||
- Runtime permissions
|
||||
- Ограничения Android 13+
|
||||
|
||||
---
|
||||
|
||||
## 12. Ошибки и edge cases
|
||||
|
||||
- потеря сети во время upload
|
||||
- повторная отправка
|
||||
- битые ссылки
|
||||
- несовпадение ключей OMEMO
|
||||
|
||||
---
|
||||
|
||||
## 13. Нефункциональные требования
|
||||
|
||||
- запуск MVP < 5 секунд
|
||||
- запись без UI лагов
|
||||
- батарея: запись ≤ 5% / минута
|
||||
|
||||
---
|
||||
|
||||
## 14. README.md (кратко)
|
||||
|
||||
README должен содержать:
|
||||
- описание проекта
|
||||
- как запустить сервер
|
||||
- как запустить iOS / Android
|
||||
- ограничения
|
||||
|
||||
---
|
||||
|
||||
## 15. Возможные расширения
|
||||
|
||||
- Push notifications
|
||||
- Groups (MUC)
|
||||
- Voice calls (Jingle + WebRTC)
|
||||
- Desktop client
|
||||
|
||||
---
|
||||
|
||||
## 16. Критерий готовности MVP
|
||||
|
||||
- iOS и Android обмениваются:
|
||||
- текстом
|
||||
- голосовыми
|
||||
- сообщения шифрованы
|
||||
- сервер не знает содержимое
|
||||
|
||||
---
|
||||
|
||||
**Этот документ считается достаточным для полной реализации проекта без дополнительных уточнений.**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user