mirror of
https://github.com/619dev/PaperPhone.git
synced 2026-05-06 14:00:33 +08:00
更新compose配置文件
This commit is contained in:
384
README_DE.md
384
README_DE.md
@@ -10,367 +10,173 @@ Eine Instant-Messaging-App im WeChat-Stil mit Ende-zu-Ende-Verschlüsselung übe
|
||||
|
||||
---
|
||||
<img width=30% height=30% src="https://raw.githubusercontent.com/619dev/PaperPhone/main/ui.jpg" alt="ui">
|
||||
## Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy |
|
||||
| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device |
|
||||
| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal |
|
||||
| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management |
|
||||
| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups |
|
||||
| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline |
|
||||
| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch |
|
||||
| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing |
|
||||
| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators |
|
||||
| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control |
|
||||
| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag |
|
||||
| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL |
|
||||
| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready |
|
||||
## Funktionen
|
||||
|
||||
| Funktion | Beschreibung |
|
||||
|----------|-------------|
|
||||
| 🔐 E2E-Verschlüsselung | Zustandsloses ECDH + XSalsa20-Poly1305 — ephemere Schlüssel pro Nachricht, Forward Secrecy |
|
||||
| 🗝️ Zero-Knowledge-Server | Server speichert nur Chiffretext; private Schlüssel verlassen niemals das Gerät |
|
||||
| 📹 Video-/Sprachanrufe | WebRTC P2P (1:1) + Mesh (Gruppe), Cloudflare TURN für NAT-Traversal |
|
||||
| 👥 Gruppenchat | Bis zu 2000 Mitglieder, Klartextnachrichten, Nicht-stören-Modus, Mitgliederverwaltung |
|
||||
| ⏱️ Auto-Löschung | 5 Stufen (nie/1T/3T/1W/1M), in DMs beidseitig einstellbar, in Gruppen nur Inhaber |
|
||||
| 🔔 Push-Benachrichtigungen | Web Push (VAPID) + OneSignal Dual-Kanal |
|
||||
| 🌐 Mehrsprachig | ZH/EN/JA/KO/FR/DE/RU/ES — automatische Erkennung + manuelle Umschaltung |
|
||||
| 📱 iOS ohne Unternehmenszertifikat | PWA über Safari „Zum Home-Bildschirm", funktioniert dauerhaft ohne Apple-Signatur |
|
||||
| 💬 Rich Messaging | Text, Bilder, Sprachnachrichten, 64-Emoji-Panel, Lesebestätigungen |
|
||||
| 🌐 Momente | Sozialer Feed: Text + bis zu 9 Fotos, Likes (Freunde-Avatare), Kommentare, Tag-basierte Sichtbarkeit |
|
||||
| 🏷️ Freunde-Tags | Mehrere Tags pro Freund (12-Farben-Palette), Kontakte nach Tags filtern |
|
||||
| 🗂️ R2-Speicher | Cloudflare R2 für Bild-/Audiodateien — optionale CDN-URL |
|
||||
| 🏗️ Self-Hosting | Docker Compose Ein-Befehl-Deployment |
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
## Technologie-Stack
|
||||
|
||||
```
|
||||
Backend (server/)
|
||||
Node.js 20 + Express + ws
|
||||
MySQL 8.0 — users, messages (persisted ciphertext)
|
||||
Redis — online presence + cross-node routing
|
||||
Cloudflare R2 — image/voice file storage (S3-compatible API)
|
||||
JWT + bcrypt authentication
|
||||
MySQL 8.0 — Benutzer, Nachrichten (verschlüsselt)
|
||||
Redis — Online-Status + Knotenübergreifendes Routing
|
||||
Cloudflare R2 — Bild-/Audio-Dateispeicher (S3-kompatible API)
|
||||
JWT + bcrypt Authentifizierung
|
||||
|
||||
Frontend (client/)
|
||||
Native HTML + Vanilla JS (ESM, no bundler required)
|
||||
Natives HTML + Vanilla JS (ESM, kein Bundler)
|
||||
libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305)
|
||||
WebRTC API — video / voice calls
|
||||
WebRTC API — Video-/Sprachanrufe
|
||||
PWA: manifest.json + Service Worker
|
||||
|
||||
Cryptographic Layer
|
||||
Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message
|
||||
Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB
|
||||
All private keys stored on-device only — never sent to the server
|
||||
Kryptographieschicht
|
||||
Zustandsloses ECDH + XSalsa20-Poly1305 — ephemeres Schlüsselpaar pro Nachricht
|
||||
4-Stufen-Schlüsselpersistenz: Speicher → localStorage → sessionStorage → IndexedDB
|
||||
Alle privaten Schlüssel nur auf dem Gerät — nie an den Server gesendet
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
## Schnellstart
|
||||
|
||||
### Option 0: Zeabur One-Click Cloud Deploy
|
||||
### Option 0: Zeabur Ein-Klick-Cloud-Deployment
|
||||
|
||||
[](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
|
||||
|
||||
> [!NOTE]
|
||||
> One manual step is required after the template deploys, otherwise login/register won't work:
|
||||
> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`)
|
||||
> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above
|
||||
> 3. Restart the client service
|
||||
> Nach dem Deployment ist ein manueller Schritt erforderlich:
|
||||
> 1. Zeabur-Konsole → **server**-Dienst → Umgebungsvariablen → `ZEABUR_WEB_URL` kopieren
|
||||
> 2. **client**-Dienst → Umgebungsvariablen → `SERVER_URL` = kopierter Wert
|
||||
> 3. Client-Dienst neu starten
|
||||
|
||||
**Known notes:**
|
||||
- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed
|
||||
- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient)
|
||||
- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service
|
||||
- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run:
|
||||
```bash
|
||||
hostname -i
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option 1: Docker Compose (Recommended — no local build needed)
|
||||
### Option 1: Docker Compose (empfohlen)
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <repo-url> && cd paperphone
|
||||
|
||||
# Copy and edit environment variables
|
||||
cp server/.env.example server/.env
|
||||
# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc.
|
||||
|
||||
# Pull images and start everything
|
||||
# Variablen bearbeiten: DB_PASS / JWT_SECRET / R2_* usw.
|
||||
docker compose up -d
|
||||
|
||||
# Check service status
|
||||
docker compose ps
|
||||
|
||||
# Open in browser
|
||||
open http://localhost
|
||||
```
|
||||
|
||||
> Pre-built images on Docker Hub:
|
||||
> - `facilisvelox/paperphone-client:latest`
|
||||
> - `facilisvelox/paperphone-server:latest`
|
||||
>
|
||||
> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required.
|
||||
> Docker-Hub-Images: `facilisvelox/paperphone-client:latest` und `facilisvelox/paperphone-server:latest`
|
||||
|
||||
### Option 2: Manual Local Start
|
||||
|
||||
#### 1. Prepare the environment
|
||||
### Option 2: Lokaler manueller Start
|
||||
|
||||
```bash
|
||||
# Copy and edit environment variables
|
||||
cp server/.env.example server/.env
|
||||
# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
|
||||
# Backend
|
||||
cd server && npm install && npm run dev # → http://localhost:3000
|
||||
|
||||
# Note: the server auto-runs schema.sql on first startup
|
||||
```
|
||||
|
||||
#### 2. Start the backend
|
||||
|
||||
```bash
|
||||
cd server
|
||||
npm install
|
||||
npm run dev # → http://localhost:3000
|
||||
```
|
||||
|
||||
#### 3. Start the frontend
|
||||
|
||||
```bash
|
||||
npx serve client -p 8080
|
||||
# → http://localhost:8080
|
||||
# Frontend
|
||||
npx serve client -p 8080 # → http://localhost:8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Video Call Configuration
|
||||
|
||||
Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal.
|
||||
|
||||
### Using Cloudflare TURN (Recommended)
|
||||
|
||||
1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App
|
||||
2. Copy the **App ID** and **App Secret** (Token Key)
|
||||
3. Add them to `server/.env`:
|
||||
## Videoanruf-Konfiguration — Cloudflare TURN
|
||||
|
||||
```env
|
||||
CF_CALLS_APP_ID=your_app_id_here
|
||||
CF_CALLS_APP_SECRET=your_app_secret_here
|
||||
```
|
||||
|
||||
4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s)
|
||||
| Typ | Transport | Empfohlen für |
|
||||
|-----|-----------|---------------|
|
||||
| 1:1 Video | WebRTC P2P + TURN | Alle Szenarien |
|
||||
| 1:1 Sprache | WebRTC P2P + TURN | Alle Szenarien |
|
||||
| Gruppenanruf | WebRTC Mesh | Bis zu 6 Teilnehmer |
|
||||
|
||||
> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration.
|
||||
|
||||
### Call Types
|
||||
|
||||
| Type | Transport | Recommended for |
|
||||
|------|-----------|-----------------|
|
||||
| 1:1 Video Call | WebRTC P2P + TURN | All scenarios |
|
||||
| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios |
|
||||
| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants |
|
||||
> **Ohne Konfiguration**: Nur STUN. LAN-Anrufe funktionieren ohne Einrichtung.
|
||||
|
||||
---
|
||||
|
||||
## Push Notification Configuration
|
||||
## Push-Benachrichtigungen
|
||||
|
||||
Offline message notifications are delivered through **two channels** for maximum delivery rate:
|
||||
|
||||
| Channel | Platforms | Configuration |
|
||||
|---------|-----------|---------------|
|
||||
| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys |
|
||||
| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key |
|
||||
|
||||
### Configuring Web Push
|
||||
|
||||
1. Generate VAPID keys (one-time):
|
||||
| Kanal | Plattformen | Konfiguration |
|
||||
|-------|-------------|---------------|
|
||||
| Web Push | Browser + iOS PWA (Safari 16.4+) | VAPID-Schlüssel |
|
||||
| OneSignal | Native Apps über Median.co | App ID + REST Key |
|
||||
|
||||
```bash
|
||||
cd server
|
||||
npx web-push generate-vapid-keys
|
||||
```
|
||||
|
||||
2. Add to `server/.env`:
|
||||
---
|
||||
|
||||
```env
|
||||
VAPID_PUBLIC_KEY=your_public_key_here
|
||||
VAPID_PRIVATE_KEY=your_private_key_here
|
||||
VAPID_SUBJECT=mailto:admin@your-domain.com
|
||||
```
|
||||
## iOS — Permanente Installation ohne Zertifikat
|
||||
|
||||
3. Restart the server — users can enable notifications from the Settings page
|
||||
|
||||
> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported.
|
||||
|
||||
### Configuring OneSignal (Median.co Native Apps)
|
||||
|
||||
1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase
|
||||
2. Enable OneSignal in Median.co and enter the App ID
|
||||
3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`:
|
||||
|
||||
```env
|
||||
ONESIGNAL_APP_ID=your_onesignal_app_id
|
||||
ONESIGNAL_REST_KEY=your_onesignal_rest_api_key
|
||||
```
|
||||
|
||||
> **When not configured**: push notifications are silently disabled — all other features work normally.
|
||||
1. Auf HTTPS-Server bereitstellen → 2. In Safari öffnen → 3. Teilen ⬆️ → 4. „Zum Home-Bildschirm"
|
||||
|
||||
---
|
||||
|
||||
## iOS — Permanent No-Cert Deployment
|
||||
## Sicherheitsmodell
|
||||
|
||||
1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs)
|
||||
2. Open `https://your.domain.com` in **Safari**
|
||||
3. Tap the Share button ⬆️ at the bottom of the screen
|
||||
4. Select **Add to Home Screen** → **Add**
|
||||
|
||||
The app behaves identically to a native app — no Apple enterprise certificate required, no expiry.
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment (Nginx)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name your.domain.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
# Frontend static files
|
||||
location / {
|
||||
root /path/to/paperphone/client;
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
|
||||
# REST API
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# WebSocket (messaging + call signalling)
|
||||
location /ws {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
}
|
||||
```
|
||||
Registrierung: IK + SPK + 10×OPK lokal generiert, öffentliche Schlüssel hochgeladen
|
||||
Nachricht: Ephemeres ECDH → X25519 → XSalsa20-Poly1305
|
||||
Server sieht: ✅ Chiffretext + Routing-Metadaten ❌ Klartext / private Schlüssel
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
## Datenbankschema
|
||||
|
||||
```
|
||||
paperphone/
|
||||
├── docker-compose.yml
|
||||
├── server/
|
||||
│ ├── .env # Environment variables (incl. Cloudflare TURN keys)
|
||||
│ └── src/
|
||||
│ ├── app.js # Express application
|
||||
│ ├── routes/
|
||||
│ │ ├── auth.js # Register / Login (incl. X3DH public key upload)
|
||||
│ │ ├── users.js # User search / Prekey bundle download
|
||||
│ │ ├── friends.js # Friend requests / Accept (incl. offline push)
|
||||
│ │ ├── groups.js # Group management
|
||||
│ │ ├── messages.js # Historical messages (paginated ciphertext)
|
||||
│ │ ├── upload.js # Cloudflare R2 file upload
|
||||
│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set)
|
||||
│ │ ├── moments.js # Moments feed (posts / likes / comments)
|
||||
│ │ ├── calls.js # TURN credential issuance
|
||||
│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal)
|
||||
│ ├── services/
|
||||
│ │ ├── push.js # Web Push VAPID service
|
||||
│ │ └── onesignal.js # OneSignal REST API service
|
||||
│ └── ws/
|
||||
│ └── wsServer.js # WebSocket router (messages + call signalling + offline push)
|
||||
│
|
||||
└── client/
|
||||
├── index.html # SPA entry + PWA meta + Median push bridge
|
||||
├── manifest.json # PWA manifest
|
||||
├── sw.js # Service Worker (offline cache + push notifications)
|
||||
└── src/
|
||||
├── style.css # Premium design system (dark/light, glassmorphism)
|
||||
├── app.js # Router + global state + incoming call listener
|
||||
├── api.js # HTTP client
|
||||
├── socket.js # WebSocket client (auto-reconnect)
|
||||
├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es)
|
||||
├── services/
|
||||
│ ├── webrtc.js # WebRTC manager — CallManager class
|
||||
│ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge)
|
||||
├── crypto/
|
||||
│ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768
|
||||
│ └── keystore.js # IndexedDB private key store
|
||||
├── pages/
|
||||
│ ├── login.js # Login / Register (key generation, language picker)
|
||||
│ ├── chats.js # Chat list
|
||||
│ ├── chat.js # Chat window (E2E encryption, call buttons)
|
||||
│ ├── groups.js # Group list (create group, search)
|
||||
│ ├── groupInfo.js # Group info (member management, DND, leave/disband)
|
||||
│ ├── contacts.js # Contacts (friend requests, online status)
|
||||
│ ├── discover.js # Discover page
|
||||
│ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA)
|
||||
│ └── call.js # Call UI (incoming / active / multi-party video)
|
||||
```
|
||||
11 Tabellen, automatisch beim ersten Start erstellt:
|
||||
|
||||
| Tabelle | Zweck |
|
||||
|---------|-------|
|
||||
| `users` | Benutzerprofile + ECDH/OPK-Schlüssel |
|
||||
| `prekeys` | X3DH-Einmal-Prekeys |
|
||||
| `friends` | Freundschaftsbeziehungen |
|
||||
| `groups` / `group_members` | Gruppenchats + Mitglieder |
|
||||
| `messages` | Verschlüsselte Nachrichten |
|
||||
| `moments` / `moment_images` | Soziale Beiträge + Bilder |
|
||||
| `moment_likes` / `moment_comments` | Likes + Kommentare |
|
||||
| `push_subscriptions` | Web Push (VAPID) |
|
||||
| `onesignal_players` | OneSignal-Geräte (Median.co) |
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
## Umgebungsvariablen
|
||||
|
||||
11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`):
|
||||
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `users` | User profiles + ECDH/OPK public keys |
|
||||
| `prekeys` | X3DH one-time prekey pool |
|
||||
| `friends` | Friendship relationships (pending / accepted / blocked) |
|
||||
| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) |
|
||||
| `messages` | Encrypted payloads (offline buffer, deletable after delivery) |
|
||||
| `moments` | Social posts (text ≤ 1024 chars) |
|
||||
| `moment_images` | Post images (up to 9 per post) |
|
||||
| `moment_likes` | Likes (unique per user per post) |
|
||||
| `moment_comments` | Comments (≤ 512 chars each) |
|
||||
| `push_subscriptions` | Web Push subscriptions (VAPID) |
|
||||
| `onesignal_players` | OneSignal device registrations (Median.co) |
|
||||
| Variable | Beschreibung | Standard |
|
||||
|----------|-------------|----------|
|
||||
| `PORT` | Server-Port | `3000` |
|
||||
| `JWT_SECRET` | JWT-Signaturschlüssel (**in Produktion ändern**) | dev_secret |
|
||||
| `DB_HOST`/`DB_PASS`/`DB_NAME` | MySQL-Verbindung | — |
|
||||
| `REDIS_HOST`/`REDIS_PASS` | Redis-Verbindung | — |
|
||||
| `R2_ACCOUNT_ID` | Cloudflare-Konto-ID | — |
|
||||
| `R2_ACCESS_KEY_ID` | R2-API-Zugriffsschlüssel | — |
|
||||
| `R2_SECRET_ACCESS_KEY` | R2-API-Geheimschlüssel | — |
|
||||
| `R2_BUCKET` | R2-Bucket-Name | — |
|
||||
| `R2_PUBLIC_URL` | Öffentliche R2-URL (optional) | — |
|
||||
| `CF_CALLS_APP_ID` | Calls App ID (optional) | — |
|
||||
| `CF_CALLS_APP_SECRET` | Calls Secret (optional) | — |
|
||||
| `VAPID_PUBLIC_KEY` | VAPID öffentlicher Schlüssel (optional) | — |
|
||||
| `VAPID_PRIVATE_KEY` | VAPID privater Schlüssel (optional) | — |
|
||||
| `ONESIGNAL_APP_ID` | OneSignal App ID (optional) | — |
|
||||
| `ONESIGNAL_REST_KEY` | OneSignal REST Key (optional) | — |
|
||||
|
||||
---
|
||||
|
||||
## Security Model
|
||||
|
||||
```
|
||||
On Registration:
|
||||
Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys)
|
||||
Public keys are uploaded; private keys stay on-device (four-tier persistence)
|
||||
|
||||
On Each Message:
|
||||
Sender fetches recipient's IK public key
|
||||
Generates a fresh ephemeral ECDH keypair (per-message)
|
||||
X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt
|
||||
Ephemeral public key sent in message header; destroyed after use
|
||||
|
||||
What the Server Sees:
|
||||
✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps)
|
||||
❌ Plaintext / private keys / ephemeral keys / call content
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PORT` | Server port | `3000` |
|
||||
| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret |
|
||||
| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — |
|
||||
| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — |
|
||||
| `R2_ACCOUNT_ID` | Cloudflare account ID | — |
|
||||
| `R2_ACCESS_KEY_ID` | R2 API token access key | — |
|
||||
| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — |
|
||||
| `R2_BUCKET` | R2 bucket name | — |
|
||||
| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — |
|
||||
| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — |
|
||||
| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — |
|
||||
| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — |
|
||||
| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — |
|
||||
| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` |
|
||||
| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — |
|
||||
| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — |
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
## Lizenz
|
||||
|
||||
MIT © PaperPhone Contributors
|
||||
|
||||
382
README_ES.md
382
README_ES.md
@@ -10,367 +10,173 @@ Una aplicación de mensajería instantánea cifrada de extremo a extremo estilo
|
||||
|
||||
---
|
||||
<img width=30% height=30% src="https://raw.githubusercontent.com/619dev/PaperPhone/main/ui.jpg" alt="ui">
|
||||
## Features
|
||||
|
||||
| Feature | Description |
|
||||
## Características
|
||||
|
||||
| Función | Descripción |
|
||||
|---------|-------------|
|
||||
| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy |
|
||||
| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device |
|
||||
| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal |
|
||||
| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management |
|
||||
| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups |
|
||||
| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline |
|
||||
| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch |
|
||||
| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing |
|
||||
| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators |
|
||||
| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control |
|
||||
| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag |
|
||||
| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL |
|
||||
| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready |
|
||||
| 🔐 Cifrado E2E | ECDH sin estado + XSalsa20-Poly1305 — claves efímeras por mensaje, secreto hacia adelante |
|
||||
| 🗝️ Servidor de conocimiento cero | El servidor solo almacena texto cifrado; las claves privadas nunca salen del dispositivo |
|
||||
| 📹 Videollamadas/voz | WebRTC P2P (1:1) + Mesh (grupo), Cloudflare TURN para atravesar NAT |
|
||||
| 👥 Chat grupal | Hasta 2000 miembros, mensajes en texto plano, modo No molestar, gestión de miembros |
|
||||
| ⏱️ Eliminación automática | 5 niveles (nunca/1d/3d/1sem/1mes), en DMs ambas partes, en grupos solo propietario |
|
||||
| 🔔 Notificaciones push | Web Push (VAPID) + OneSignal canal doble |
|
||||
| 🌐 Multilingüe | ZH/EN/JA/KO/FR/DE/RU/ES — detección automática + selección manual |
|
||||
| 📱 iOS sin certificado empresarial | PWA vía Safari "Añadir a inicio", sin firma de Apple |
|
||||
| 💬 Mensajería rica | Texto, imágenes, mensajes de voz, 64 emojis, confirmaciones de lectura |
|
||||
| 🌐 Momentos | Feed social: texto + hasta 9 fotos, likes (avatares de amigos), comentarios, visibilidad por etiquetas |
|
||||
| 🏷️ Etiquetas de amigos | Múltiples etiquetas por amigo (paleta de 12 colores), filtrar contactos por etiqueta |
|
||||
| 🗂️ Almacenamiento R2 | Cloudflare R2 para imágenes/audio — URL CDN opcional |
|
||||
| 🏗️ Auto-alojable | Despliegue Docker Compose en un comando |
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
## Stack tecnológico
|
||||
|
||||
```
|
||||
Backend (server/)
|
||||
Node.js 20 + Express + ws
|
||||
MySQL 8.0 — users, messages (persisted ciphertext)
|
||||
Redis — online presence + cross-node routing
|
||||
Cloudflare R2 — image/voice file storage (S3-compatible API)
|
||||
JWT + bcrypt authentication
|
||||
MySQL 8.0 — usuarios, mensajes (texto cifrado)
|
||||
Redis — presencia en línea + enrutamiento entre nodos
|
||||
Cloudflare R2 — almacenamiento de archivos (API compatible S3)
|
||||
Autenticación JWT + bcrypt
|
||||
|
||||
Frontend (client/)
|
||||
Native HTML + Vanilla JS (ESM, no bundler required)
|
||||
HTML nativo + Vanilla JS (ESM, sin bundler)
|
||||
libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305)
|
||||
WebRTC API — video / voice calls
|
||||
API WebRTC — videollamadas / llamadas de voz
|
||||
PWA: manifest.json + Service Worker
|
||||
|
||||
Cryptographic Layer
|
||||
Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message
|
||||
Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB
|
||||
All private keys stored on-device only — never sent to the server
|
||||
Capa criptográfica
|
||||
ECDH sin estado + XSalsa20-Poly1305 — par de claves efímeras por mensaje
|
||||
Persistencia de claves en 4 niveles: memoria → localStorage → sessionStorage → IndexedDB
|
||||
Todas las claves privadas solo en el dispositivo — nunca se envían al servidor
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
## Inicio rápido
|
||||
|
||||
### Option 0: Zeabur One-Click Cloud Deploy
|
||||
### Opción 0: Zeabur — Despliegue en la nube con un clic
|
||||
|
||||
[](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
|
||||
|
||||
> [!NOTE]
|
||||
> One manual step is required after the template deploys, otherwise login/register won't work:
|
||||
> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`)
|
||||
> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above
|
||||
> 3. Restart the client service
|
||||
> Se requiere un paso manual después del despliegue:
|
||||
> 1. Consola Zeabur → servicio **server** → Variables de entorno → copiar el valor de `ZEABUR_WEB_URL`
|
||||
> 2. Servicio **client** → Variables de entorno → agregar `SERVER_URL` = valor copiado
|
||||
> 3. Reiniciar el servicio client
|
||||
|
||||
**Known notes:**
|
||||
- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed
|
||||
- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient)
|
||||
- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service
|
||||
- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run:
|
||||
```bash
|
||||
hostname -i
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option 1: Docker Compose (Recommended — no local build needed)
|
||||
### Opción 1: Docker Compose (recomendado)
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <repo-url> && cd paperphone
|
||||
|
||||
# Copy and edit environment variables
|
||||
cp server/.env.example server/.env
|
||||
# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc.
|
||||
|
||||
# Pull images and start everything
|
||||
# Editar: DB_PASS / JWT_SECRET / R2_* etc.
|
||||
docker compose up -d
|
||||
|
||||
# Check service status
|
||||
docker compose ps
|
||||
|
||||
# Open in browser
|
||||
open http://localhost
|
||||
```
|
||||
|
||||
> Pre-built images on Docker Hub:
|
||||
> - `facilisvelox/paperphone-client:latest`
|
||||
> - `facilisvelox/paperphone-server:latest`
|
||||
>
|
||||
> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required.
|
||||
> Imágenes Docker Hub: `facilisvelox/paperphone-client:latest` y `facilisvelox/paperphone-server:latest`
|
||||
|
||||
### Option 2: Manual Local Start
|
||||
|
||||
#### 1. Prepare the environment
|
||||
### Opción 2: Inicio local manual
|
||||
|
||||
```bash
|
||||
# Copy and edit environment variables
|
||||
cp server/.env.example server/.env
|
||||
# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
|
||||
# Backend
|
||||
cd server && npm install && npm run dev # → http://localhost:3000
|
||||
|
||||
# Note: the server auto-runs schema.sql on first startup
|
||||
```
|
||||
|
||||
#### 2. Start the backend
|
||||
|
||||
```bash
|
||||
cd server
|
||||
npm install
|
||||
npm run dev # → http://localhost:3000
|
||||
```
|
||||
|
||||
#### 3. Start the frontend
|
||||
|
||||
```bash
|
||||
npx serve client -p 8080
|
||||
# → http://localhost:8080
|
||||
# Frontend
|
||||
npx serve client -p 8080 # → http://localhost:8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Video Call Configuration
|
||||
|
||||
Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal.
|
||||
|
||||
### Using Cloudflare TURN (Recommended)
|
||||
|
||||
1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App
|
||||
2. Copy the **App ID** and **App Secret** (Token Key)
|
||||
3. Add them to `server/.env`:
|
||||
## Configuración de videollamadas — Cloudflare TURN
|
||||
|
||||
```env
|
||||
CF_CALLS_APP_ID=your_app_id_here
|
||||
CF_CALLS_APP_SECRET=your_app_secret_here
|
||||
```
|
||||
|
||||
4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s)
|
||||
| Tipo | Transporte | Recomendado para |
|
||||
|------|-----------|------------------|
|
||||
| Video 1:1 | WebRTC P2P + TURN | Todos los escenarios |
|
||||
| Voz 1:1 | WebRTC P2P + TURN | Todos los escenarios |
|
||||
| Llamada grupal | WebRTC Mesh | Hasta 6 participantes |
|
||||
|
||||
> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration.
|
||||
|
||||
### Call Types
|
||||
|
||||
| Type | Transport | Recommended for |
|
||||
|------|-----------|-----------------|
|
||||
| 1:1 Video Call | WebRTC P2P + TURN | All scenarios |
|
||||
| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios |
|
||||
| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants |
|
||||
> **Sin configuración**: solo STUN. Las llamadas en LAN funcionan sin configuración adicional.
|
||||
|
||||
---
|
||||
|
||||
## Push Notification Configuration
|
||||
## Notificaciones push
|
||||
|
||||
Offline message notifications are delivered through **two channels** for maximum delivery rate:
|
||||
|
||||
| Channel | Platforms | Configuration |
|
||||
|---------|-----------|---------------|
|
||||
| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys |
|
||||
| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key |
|
||||
|
||||
### Configuring Web Push
|
||||
|
||||
1. Generate VAPID keys (one-time):
|
||||
| Canal | Plataformas | Configuración |
|
||||
|-------|-------------|---------------|
|
||||
| Web Push | Navegadores + iOS PWA (Safari 16.4+) | Claves VAPID |
|
||||
| OneSignal | Apps nativas Median.co | App ID + REST Key |
|
||||
|
||||
```bash
|
||||
cd server
|
||||
npx web-push generate-vapid-keys
|
||||
```
|
||||
|
||||
2. Add to `server/.env`:
|
||||
---
|
||||
|
||||
```env
|
||||
VAPID_PUBLIC_KEY=your_public_key_here
|
||||
VAPID_PRIVATE_KEY=your_private_key_here
|
||||
VAPID_SUBJECT=mailto:admin@your-domain.com
|
||||
```
|
||||
## iOS — Instalación permanente sin certificado
|
||||
|
||||
3. Restart the server — users can enable notifications from the Settings page
|
||||
|
||||
> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported.
|
||||
|
||||
### Configuring OneSignal (Median.co Native Apps)
|
||||
|
||||
1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase
|
||||
2. Enable OneSignal in Median.co and enter the App ID
|
||||
3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`:
|
||||
|
||||
```env
|
||||
ONESIGNAL_APP_ID=your_onesignal_app_id
|
||||
ONESIGNAL_REST_KEY=your_onesignal_rest_api_key
|
||||
```
|
||||
|
||||
> **When not configured**: push notifications are silently disabled — all other features work normally.
|
||||
1. Desplegar en servidor HTTPS → 2. Abrir en Safari → 3. Compartir ⬆️ → 4. «Añadir a pantalla de inicio»
|
||||
|
||||
---
|
||||
|
||||
## iOS — Permanent No-Cert Deployment
|
||||
## Modelo de seguridad
|
||||
|
||||
1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs)
|
||||
2. Open `https://your.domain.com` in **Safari**
|
||||
3. Tap the Share button ⬆️ at the bottom of the screen
|
||||
4. Select **Add to Home Screen** → **Add**
|
||||
|
||||
The app behaves identically to a native app — no Apple enterprise certificate required, no expiry.
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment (Nginx)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name your.domain.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
# Frontend static files
|
||||
location / {
|
||||
root /path/to/paperphone/client;
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
|
||||
# REST API
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# WebSocket (messaging + call signalling)
|
||||
location /ws {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
}
|
||||
```
|
||||
Registro: IK + SPK + 10×OPK generados localmente, claves públicas subidas
|
||||
Mensaje: ECDH efímero → X25519 → XSalsa20-Poly1305
|
||||
El servidor ve: ✅ texto cifrado + metadatos de enrutamiento ❌ texto plano / claves privadas
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
## Esquema de base de datos
|
||||
|
||||
```
|
||||
paperphone/
|
||||
├── docker-compose.yml
|
||||
├── server/
|
||||
│ ├── .env # Environment variables (incl. Cloudflare TURN keys)
|
||||
│ └── src/
|
||||
│ ├── app.js # Express application
|
||||
│ ├── routes/
|
||||
│ │ ├── auth.js # Register / Login (incl. X3DH public key upload)
|
||||
│ │ ├── users.js # User search / Prekey bundle download
|
||||
│ │ ├── friends.js # Friend requests / Accept (incl. offline push)
|
||||
│ │ ├── groups.js # Group management
|
||||
│ │ ├── messages.js # Historical messages (paginated ciphertext)
|
||||
│ │ ├── upload.js # Cloudflare R2 file upload
|
||||
│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set)
|
||||
│ │ ├── moments.js # Moments feed (posts / likes / comments)
|
||||
│ │ ├── calls.js # TURN credential issuance
|
||||
│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal)
|
||||
│ ├── services/
|
||||
│ │ ├── push.js # Web Push VAPID service
|
||||
│ │ └── onesignal.js # OneSignal REST API service
|
||||
│ └── ws/
|
||||
│ └── wsServer.js # WebSocket router (messages + call signalling + offline push)
|
||||
│
|
||||
└── client/
|
||||
├── index.html # SPA entry + PWA meta + Median push bridge
|
||||
├── manifest.json # PWA manifest
|
||||
├── sw.js # Service Worker (offline cache + push notifications)
|
||||
└── src/
|
||||
├── style.css # Premium design system (dark/light, glassmorphism)
|
||||
├── app.js # Router + global state + incoming call listener
|
||||
├── api.js # HTTP client
|
||||
├── socket.js # WebSocket client (auto-reconnect)
|
||||
├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es)
|
||||
├── services/
|
||||
│ ├── webrtc.js # WebRTC manager — CallManager class
|
||||
│ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge)
|
||||
├── crypto/
|
||||
│ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768
|
||||
│ └── keystore.js # IndexedDB private key store
|
||||
├── pages/
|
||||
│ ├── login.js # Login / Register (key generation, language picker)
|
||||
│ ├── chats.js # Chat list
|
||||
│ ├── chat.js # Chat window (E2E encryption, call buttons)
|
||||
│ ├── groups.js # Group list (create group, search)
|
||||
│ ├── groupInfo.js # Group info (member management, DND, leave/disband)
|
||||
│ ├── contacts.js # Contacts (friend requests, online status)
|
||||
│ ├── discover.js # Discover page
|
||||
│ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA)
|
||||
│ └── call.js # Call UI (incoming / active / multi-party video)
|
||||
```
|
||||
11 tablas, creadas automáticamente en el primer inicio:
|
||||
|
||||
| Tabla | Propósito |
|
||||
|-------|-----------|
|
||||
| `users` | Perfiles + claves públicas ECDH/OPK |
|
||||
| `prekeys` | Pre-claves X3DH de un solo uso |
|
||||
| `friends` | Relaciones de amistad |
|
||||
| `groups` / `group_members` | Grupos + miembros |
|
||||
| `messages` | Mensajes cifrados |
|
||||
| `moments` / `moment_images` | Publicaciones + imágenes |
|
||||
| `moment_likes` / `moment_comments` | Likes + comentarios |
|
||||
| `push_subscriptions` | Web Push (VAPID) |
|
||||
| `onesignal_players` | Dispositivos OneSignal |
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
## Variables de entorno
|
||||
|
||||
11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`):
|
||||
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `users` | User profiles + ECDH/OPK public keys |
|
||||
| `prekeys` | X3DH one-time prekey pool |
|
||||
| `friends` | Friendship relationships (pending / accepted / blocked) |
|
||||
| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) |
|
||||
| `messages` | Encrypted payloads (offline buffer, deletable after delivery) |
|
||||
| `moments` | Social posts (text ≤ 1024 chars) |
|
||||
| `moment_images` | Post images (up to 9 per post) |
|
||||
| `moment_likes` | Likes (unique per user per post) |
|
||||
| `moment_comments` | Comments (≤ 512 chars each) |
|
||||
| `push_subscriptions` | Web Push subscriptions (VAPID) |
|
||||
| `onesignal_players` | OneSignal device registrations (Median.co) |
|
||||
| Variable | Descripción | Predeterminado |
|
||||
|----------|-------------|----------------|
|
||||
| `PORT` | Puerto del servidor | `3000` |
|
||||
| `JWT_SECRET` | Clave de firma JWT (**cambiar en producción**) | dev_secret |
|
||||
| `DB_HOST`/`DB_PASS`/`DB_NAME` | Conexión MySQL | — |
|
||||
| `REDIS_HOST`/`REDIS_PASS` | Conexión Redis | — |
|
||||
| `R2_ACCOUNT_ID` | ID de cuenta Cloudflare | — |
|
||||
| `R2_ACCESS_KEY_ID` | Clave de acceso R2 API | — |
|
||||
| `R2_SECRET_ACCESS_KEY` | Clave secreta R2 API | — |
|
||||
| `R2_BUCKET` | Nombre del bucket R2 | — |
|
||||
| `R2_PUBLIC_URL` | URL pública R2 (opc.) | — |
|
||||
| `CF_CALLS_APP_ID` | Calls App ID (opc.) | — |
|
||||
| `CF_CALLS_APP_SECRET` | Calls Secret (opc.) | — |
|
||||
| `VAPID_PUBLIC_KEY` | Clave pública VAPID (opc.) | — |
|
||||
| `VAPID_PRIVATE_KEY` | Clave privada VAPID (opc.) | — |
|
||||
| `ONESIGNAL_APP_ID` | OneSignal App ID (opc.) | — |
|
||||
| `ONESIGNAL_REST_KEY` | OneSignal REST Key (opc.) | — |
|
||||
|
||||
---
|
||||
|
||||
## Security Model
|
||||
|
||||
```
|
||||
On Registration:
|
||||
Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys)
|
||||
Public keys are uploaded; private keys stay on-device (four-tier persistence)
|
||||
|
||||
On Each Message:
|
||||
Sender fetches recipient's IK public key
|
||||
Generates a fresh ephemeral ECDH keypair (per-message)
|
||||
X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt
|
||||
Ephemeral public key sent in message header; destroyed after use
|
||||
|
||||
What the Server Sees:
|
||||
✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps)
|
||||
❌ Plaintext / private keys / ephemeral keys / call content
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PORT` | Server port | `3000` |
|
||||
| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret |
|
||||
| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — |
|
||||
| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — |
|
||||
| `R2_ACCOUNT_ID` | Cloudflare account ID | — |
|
||||
| `R2_ACCESS_KEY_ID` | R2 API token access key | — |
|
||||
| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — |
|
||||
| `R2_BUCKET` | R2 bucket name | — |
|
||||
| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — |
|
||||
| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — |
|
||||
| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — |
|
||||
| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — |
|
||||
| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — |
|
||||
| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` |
|
||||
| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — |
|
||||
| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — |
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
## Licencia
|
||||
|
||||
MIT © PaperPhone Contributors
|
||||
|
||||
372
README_FR.md
372
README_FR.md
@@ -10,367 +10,147 @@ Une application de messagerie instantanée chiffrée de bout en bout, style WeCh
|
||||
|
||||
---
|
||||
<img width=30% height=30% src="https://raw.githubusercontent.com/619dev/PaperPhone/main/ui.jpg" alt="ui">
|
||||
## Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy |
|
||||
| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device |
|
||||
| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal |
|
||||
| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management |
|
||||
| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups |
|
||||
| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline |
|
||||
| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch |
|
||||
| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing |
|
||||
| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators |
|
||||
| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control |
|
||||
| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag |
|
||||
| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL |
|
||||
| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready |
|
||||
## Fonctionnalités
|
||||
|
||||
| Fonction | Description |
|
||||
|----------|-------------|
|
||||
| 🔐 Chiffrement E2E | ECDH sans état + XSalsa20-Poly1305 — clés éphémères par message, secret de transmission |
|
||||
| 🗝️ Serveur à connaissance nulle | Le serveur ne stocke que le texte chiffré ; les clés privées ne quittent jamais l'appareil |
|
||||
| 📹 Appels vidéo/audio | WebRTC P2P (1:1) + Mesh (groupe), Cloudflare TURN pour traversée NAT |
|
||||
| 👥 Chat de groupe | Jusqu'à 2000 membres, messages en texte brut, mode Ne pas déranger |
|
||||
| ⏱️ Suppression auto | 5 niveaux (jamais/1j/3j/1sem/1mois) |
|
||||
| 🔔 Notifications push | Web Push (VAPID) + OneSignal double canal |
|
||||
| 🌐 Multilingue | 中/EN/日/한/FR/DE/RU/ES — détection auto + sélection manuelle |
|
||||
| 📱 iOS PWA | Safari « Ajouter à l'écran d'accueil », sans certificat Apple |
|
||||
| 💬 Messagerie riche | Texte, images, audio, 64 émojis, accusés de réception |
|
||||
| 🌐 Moments | Fil social : texte + 9 photos, likes (avatars), commentaires, visibilité par tags |
|
||||
| 🏷️ Tags d'amis | Plusieurs tags par ami (12 couleurs), filtrage des contacts |
|
||||
| 🗂️ Stockage R2 | Cloudflare R2 pour images/audio — CDN optionnel |
|
||||
| 🏗️ Auto-hébergeable | Docker Compose en une commande |
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
## Stack technique
|
||||
|
||||
```
|
||||
Backend (server/)
|
||||
Node.js 20 + Express + ws
|
||||
MySQL 8.0 — users, messages (persisted ciphertext)
|
||||
Redis — online presence + cross-node routing
|
||||
Cloudflare R2 — image/voice file storage (S3-compatible API)
|
||||
JWT + bcrypt authentication
|
||||
MySQL 8.0 — utilisateurs, messages chiffrés
|
||||
Redis — présence en ligne + routage inter-nœuds
|
||||
Cloudflare R2 — stockage fichiers (API S3)
|
||||
JWT + bcrypt
|
||||
|
||||
Frontend (client/)
|
||||
Native HTML + Vanilla JS (ESM, no bundler required)
|
||||
libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305)
|
||||
WebRTC API — video / voice calls
|
||||
PWA: manifest.json + Service Worker
|
||||
HTML natif + Vanilla JS (ESM, sans bundler)
|
||||
libsodium-wrappers (WebAssembly)
|
||||
WebRTC API — appels vidéo/audio
|
||||
PWA : manifest.json + Service Worker
|
||||
|
||||
Cryptographic Layer
|
||||
Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message
|
||||
Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB
|
||||
All private keys stored on-device only — never sent to the server
|
||||
Cryptographie
|
||||
ECDH sans état + XSalsa20-Poly1305 — clés éphémères par message
|
||||
Persistance 4 niveaux : mémoire → localStorage → sessionStorage → IndexedDB
|
||||
Clés privées uniquement sur l'appareil
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
## Démarrage rapide
|
||||
|
||||
### Option 0: Zeabur One-Click Cloud Deploy
|
||||
### Option 0 : Zeabur en un clic
|
||||
|
||||
[](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
|
||||
|
||||
> [!NOTE]
|
||||
> One manual step is required after the template deploys, otherwise login/register won't work:
|
||||
> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`)
|
||||
> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above
|
||||
> 3. Restart the client service
|
||||
> Étape manuelle requise après déploiement :
|
||||
> 1. Console Zeabur → **server** → Variables → copier `ZEABUR_WEB_URL`
|
||||
> 2. **client** → Variables → ajouter `SERVER_URL` = valeur copiée
|
||||
> 3. Redémarrer le client
|
||||
|
||||
**Known notes:**
|
||||
- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed
|
||||
- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient)
|
||||
- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service
|
||||
- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run:
|
||||
```bash
|
||||
hostname -i
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option 1: Docker Compose (Recommended — no local build needed)
|
||||
### Option 1 : Docker Compose (recommandé)
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <repo-url> && cd paperphone
|
||||
|
||||
# Copy and edit environment variables
|
||||
cp server/.env.example server/.env
|
||||
# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc.
|
||||
|
||||
# Pull images and start everything
|
||||
# Éditer les variables
|
||||
docker compose up -d
|
||||
|
||||
# Check service status
|
||||
docker compose ps
|
||||
|
||||
# Open in browser
|
||||
open http://localhost
|
||||
```
|
||||
|
||||
> Pre-built images on Docker Hub:
|
||||
> - `facilisvelox/paperphone-client:latest`
|
||||
> - `facilisvelox/paperphone-server:latest`
|
||||
>
|
||||
> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required.
|
||||
|
||||
### Option 2: Manual Local Start
|
||||
|
||||
#### 1. Prepare the environment
|
||||
### Option 2 : Démarrage local
|
||||
|
||||
```bash
|
||||
# Copy and edit environment variables
|
||||
cp server/.env.example server/.env
|
||||
# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
|
||||
# Backend
|
||||
cd server && npm install && npm run dev
|
||||
|
||||
# Note: the server auto-runs schema.sql on first startup
|
||||
```
|
||||
|
||||
#### 2. Start the backend
|
||||
|
||||
```bash
|
||||
cd server
|
||||
npm install
|
||||
npm run dev # → http://localhost:3000
|
||||
```
|
||||
|
||||
#### 3. Start the frontend
|
||||
|
||||
```bash
|
||||
# Frontend
|
||||
npx serve client -p 8080
|
||||
# → http://localhost:8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Video Call Configuration
|
||||
|
||||
Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal.
|
||||
|
||||
### Using Cloudflare TURN (Recommended)
|
||||
|
||||
1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App
|
||||
2. Copy the **App ID** and **App Secret** (Token Key)
|
||||
3. Add them to `server/.env`:
|
||||
## Appels vidéo — Cloudflare TURN
|
||||
|
||||
```env
|
||||
CF_CALLS_APP_ID=your_app_id_here
|
||||
CF_CALLS_APP_SECRET=your_app_secret_here
|
||||
```
|
||||
|
||||
4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s)
|
||||
|
||||
> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration.
|
||||
|
||||
### Call Types
|
||||
|
||||
| Type | Transport | Recommended for |
|
||||
|------|-----------|-----------------|
|
||||
| 1:1 Video Call | WebRTC P2P + TURN | All scenarios |
|
||||
| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios |
|
||||
| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants |
|
||||
> Sans configuration : STUN uniquement, appels LAN fonctionnent.
|
||||
|
||||
---
|
||||
|
||||
## Push Notification Configuration
|
||||
## Notifications push
|
||||
|
||||
Offline message notifications are delivered through **two channels** for maximum delivery rate:
|
||||
|
||||
| Channel | Platforms | Configuration |
|
||||
|---------|-----------|---------------|
|
||||
| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys |
|
||||
| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key |
|
||||
|
||||
### Configuring Web Push
|
||||
|
||||
1. Generate VAPID keys (one-time):
|
||||
| Canal | Plateformes | Configuration |
|
||||
|-------|-------------|---------------|
|
||||
| Web Push | Navigateurs + iOS PWA 16.4+ | Clés VAPID |
|
||||
| OneSignal | Apps Median.co | App ID + REST Key |
|
||||
|
||||
```bash
|
||||
cd server
|
||||
npx web-push generate-vapid-keys
|
||||
```
|
||||
|
||||
2. Add to `server/.env`:
|
||||
---
|
||||
|
||||
```env
|
||||
VAPID_PUBLIC_KEY=your_public_key_here
|
||||
VAPID_PRIVATE_KEY=your_private_key_here
|
||||
VAPID_SUBJECT=mailto:admin@your-domain.com
|
||||
```
|
||||
## iOS — Sans certificat
|
||||
|
||||
3. Restart the server — users can enable notifications from the Settings page
|
||||
|
||||
> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported.
|
||||
|
||||
### Configuring OneSignal (Median.co Native Apps)
|
||||
|
||||
1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase
|
||||
2. Enable OneSignal in Median.co and enter the App ID
|
||||
3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`:
|
||||
|
||||
```env
|
||||
ONESIGNAL_APP_ID=your_onesignal_app_id
|
||||
ONESIGNAL_REST_KEY=your_onesignal_rest_api_key
|
||||
```
|
||||
|
||||
> **When not configured**: push notifications are silently disabled — all other features work normally.
|
||||
1. Déployer avec HTTPS → 2. Ouvrir dans Safari → 3. Partager ⬆️ → 4. « Ajouter à l'écran d'accueil »
|
||||
|
||||
---
|
||||
|
||||
## iOS — Permanent No-Cert Deployment
|
||||
## Sécurité
|
||||
|
||||
1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs)
|
||||
2. Open `https://your.domain.com` in **Safari**
|
||||
3. Tap the Share button ⬆️ at the bottom of the screen
|
||||
4. Select **Add to Home Screen** → **Add**
|
||||
|
||||
The app behaves identically to a native app — no Apple enterprise certificate required, no expiry.
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment (Nginx)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name your.domain.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
# Frontend static files
|
||||
location / {
|
||||
root /path/to/paperphone/client;
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
|
||||
# REST API
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# WebSocket (messaging + call signalling)
|
||||
location /ws {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
}
|
||||
```
|
||||
Inscription : IK + SPK + 10×OPK générés localement, clés publiques uploadées
|
||||
Message : ECDH éphémère → X25519 → XSalsa20-Poly1305
|
||||
Serveur voit : ✅ texte chiffré + métadonnées ❌ texte clair / clés privées
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
## Variables d'environnement
|
||||
|
||||
```
|
||||
paperphone/
|
||||
├── docker-compose.yml
|
||||
├── server/
|
||||
│ ├── .env # Environment variables (incl. Cloudflare TURN keys)
|
||||
│ └── src/
|
||||
│ ├── app.js # Express application
|
||||
│ ├── routes/
|
||||
│ │ ├── auth.js # Register / Login (incl. X3DH public key upload)
|
||||
│ │ ├── users.js # User search / Prekey bundle download
|
||||
│ │ ├── friends.js # Friend requests / Accept (incl. offline push)
|
||||
│ │ ├── groups.js # Group management
|
||||
│ │ ├── messages.js # Historical messages (paginated ciphertext)
|
||||
│ │ ├── upload.js # Cloudflare R2 file upload
|
||||
│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set)
|
||||
│ │ ├── moments.js # Moments feed (posts / likes / comments)
|
||||
│ │ ├── calls.js # TURN credential issuance
|
||||
│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal)
|
||||
│ ├── services/
|
||||
│ │ ├── push.js # Web Push VAPID service
|
||||
│ │ └── onesignal.js # OneSignal REST API service
|
||||
│ └── ws/
|
||||
│ └── wsServer.js # WebSocket router (messages + call signalling + offline push)
|
||||
│
|
||||
└── client/
|
||||
├── index.html # SPA entry + PWA meta + Median push bridge
|
||||
├── manifest.json # PWA manifest
|
||||
├── sw.js # Service Worker (offline cache + push notifications)
|
||||
└── src/
|
||||
├── style.css # Premium design system (dark/light, glassmorphism)
|
||||
├── app.js # Router + global state + incoming call listener
|
||||
├── api.js # HTTP client
|
||||
├── socket.js # WebSocket client (auto-reconnect)
|
||||
├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es)
|
||||
├── services/
|
||||
│ ├── webrtc.js # WebRTC manager — CallManager class
|
||||
│ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge)
|
||||
├── crypto/
|
||||
│ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768
|
||||
│ └── keystore.js # IndexedDB private key store
|
||||
├── pages/
|
||||
│ ├── login.js # Login / Register (key generation, language picker)
|
||||
│ ├── chats.js # Chat list
|
||||
│ ├── chat.js # Chat window (E2E encryption, call buttons)
|
||||
│ ├── groups.js # Group list (create group, search)
|
||||
│ ├── groupInfo.js # Group info (member management, DND, leave/disband)
|
||||
│ ├── contacts.js # Contacts (friend requests, online status)
|
||||
│ ├── discover.js # Discover page
|
||||
│ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA)
|
||||
│ └── call.js # Call UI (incoming / active / multi-party video)
|
||||
```
|
||||
| Variable | Description | Défaut |
|
||||
|----------|-------------|--------|
|
||||
| `PORT` | Port serveur | `3000` |
|
||||
| `JWT_SECRET` | Clé JWT (**changer en prod**) | dev_secret |
|
||||
| `DB_HOST`/`DB_PASS`/`DB_NAME` | MySQL | — |
|
||||
| `REDIS_HOST`/`REDIS_PASS` | Redis | — |
|
||||
| `R2_ACCOUNT_ID` | Cloudflare ID | — |
|
||||
| `R2_ACCESS_KEY_ID` | Clé d'accès R2 | — |
|
||||
| `R2_SECRET_ACCESS_KEY` | Clé secrète R2 | — |
|
||||
| `R2_BUCKET` | Nom du bucket | — |
|
||||
| `R2_PUBLIC_URL` | URL publique (optionnel) | — |
|
||||
| `CF_CALLS_APP_ID` | Calls App ID (optionnel) | — |
|
||||
| `CF_CALLS_APP_SECRET` | Calls Secret (optionnel) | — |
|
||||
| `VAPID_PUBLIC_KEY` | Clé publique VAPID (optionnel) | — |
|
||||
| `VAPID_PRIVATE_KEY` | Clé privée VAPID (optionnel) | — |
|
||||
| `ONESIGNAL_APP_ID` | OneSignal ID (optionnel) | — |
|
||||
| `ONESIGNAL_REST_KEY` | OneSignal Key (optionnel) | — |
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
|
||||
11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`):
|
||||
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `users` | User profiles + ECDH/OPK public keys |
|
||||
| `prekeys` | X3DH one-time prekey pool |
|
||||
| `friends` | Friendship relationships (pending / accepted / blocked) |
|
||||
| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) |
|
||||
| `messages` | Encrypted payloads (offline buffer, deletable after delivery) |
|
||||
| `moments` | Social posts (text ≤ 1024 chars) |
|
||||
| `moment_images` | Post images (up to 9 per post) |
|
||||
| `moment_likes` | Likes (unique per user per post) |
|
||||
| `moment_comments` | Comments (≤ 512 chars each) |
|
||||
| `push_subscriptions` | Web Push subscriptions (VAPID) |
|
||||
| `onesignal_players` | OneSignal device registrations (Median.co) |
|
||||
|
||||
---
|
||||
|
||||
## Security Model
|
||||
|
||||
```
|
||||
On Registration:
|
||||
Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys)
|
||||
Public keys are uploaded; private keys stay on-device (four-tier persistence)
|
||||
|
||||
On Each Message:
|
||||
Sender fetches recipient's IK public key
|
||||
Generates a fresh ephemeral ECDH keypair (per-message)
|
||||
X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt
|
||||
Ephemeral public key sent in message header; destroyed after use
|
||||
|
||||
What the Server Sees:
|
||||
✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps)
|
||||
❌ Plaintext / private keys / ephemeral keys / call content
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PORT` | Server port | `3000` |
|
||||
| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret |
|
||||
| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — |
|
||||
| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — |
|
||||
| `R2_ACCOUNT_ID` | Cloudflare account ID | — |
|
||||
| `R2_ACCESS_KEY_ID` | R2 API token access key | — |
|
||||
| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — |
|
||||
| `R2_BUCKET` | R2 bucket name | — |
|
||||
| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — |
|
||||
| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — |
|
||||
| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — |
|
||||
| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — |
|
||||
| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — |
|
||||
| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` |
|
||||
| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — |
|
||||
| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — |
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
## Licence
|
||||
|
||||
MIT © PaperPhone Contributors
|
||||
|
||||
357
README_JA.md
357
README_JA.md
@@ -10,112 +10,113 @@ WeChatスタイルのエンドツーエンド暗号化インスタントメッ
|
||||
|
||||
---
|
||||
<img width=30% height=30% src="https://raw.githubusercontent.com/619dev/PaperPhone/main/ui.jpg" alt="ui">
|
||||
## Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy |
|
||||
| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device |
|
||||
| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal |
|
||||
| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management |
|
||||
| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups |
|
||||
| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline |
|
||||
| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch |
|
||||
| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing |
|
||||
| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators |
|
||||
| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control |
|
||||
| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag |
|
||||
| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL |
|
||||
| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready |
|
||||
## 機能一覧
|
||||
|
||||
| 機能 | 説明 |
|
||||
|------|------|
|
||||
| 🔐 エンドツーエンド暗号化 | ステートレス ECDH + XSalsa20-Poly1305 — メッセージごとの一時鍵、前方秘匿性 |
|
||||
| 🗝️ ゼロ知識サーバー | サーバーは暗号文のみ保存。秘密鍵はデバイスから離れません |
|
||||
| 📹 ビデオ/音声通話 | WebRTC P2P(1:1)+ Mesh(グループ)、Cloudflare TURN による NAT トラバーサル |
|
||||
| 👥 グループチャット | 最大2000人、プレーンテキストメッセージ(暗号化なし)、通知オフモード、メンバー管理 |
|
||||
| ⏱️ メッセージ自動削除 | 5段階(なし/1日/3日/1週間/1ヶ月)、DM は双方設定可、グループはオーナーのみ |
|
||||
| 🔔 プッシュ通知 | Web Push (VAPID) + OneSignal デュアルチャネル — オフラインでも通知 |
|
||||
| 🌐 多言語対応 | 中国語・英語・日本語・韓国語・フランス語・ドイツ語・ロシア語・スペイン語(自動検出+手動切替) |
|
||||
| 📱 iOS 永久署名不要 | PWA — Safari「ホーム画面に追加」でエンタープライズ証明書なしで利用可能 |
|
||||
| 💬 リッチメッセージ | テキスト・画像・音声・絵文字パネル(64種)・既読確認 |
|
||||
| 🌐 モーメンツ | テキスト+最大9枚写真、いいね(友達アバター表示)、コメント、タグベースの公開範囲制御 |
|
||||
| 🏷️ フレンドタグ | 友達に複数タグを設定(12色プリセット)、タグ別に連絡先をフィルタリング |
|
||||
| 🗂️ R2 オブジェクトストレージ | Cloudflare R2 で画像/音声ファイルを保存 — オプションの CDN 直リンク |
|
||||
| 🏗️ セルフホスト対応 | Docker Compose ワンコマンドデプロイ、Node.js + Redis マルチノード対応 |
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
## 技術スタック
|
||||
|
||||
```
|
||||
Backend (server/)
|
||||
バックエンド (server/)
|
||||
Node.js 20 + Express + ws
|
||||
MySQL 8.0 — users, messages (persisted ciphertext)
|
||||
Redis — online presence + cross-node routing
|
||||
Cloudflare R2 — image/voice file storage (S3-compatible API)
|
||||
JWT + bcrypt authentication
|
||||
MySQL 8.0 — ユーザー・メッセージの永続化
|
||||
Redis — オンラインプレゼンス+クロスノードルーティング
|
||||
Cloudflare R2 — 画像/音声ファイルストレージ(S3互換API)
|
||||
JWT + bcrypt 認証
|
||||
|
||||
Frontend (client/)
|
||||
Native HTML + Vanilla JS (ESM, no bundler required)
|
||||
libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305)
|
||||
WebRTC API — video / voice calls
|
||||
フロントエンド (client/)
|
||||
ネイティブ HTML + Vanilla JS(ESM、バンドラー不要)
|
||||
libsodium-wrappers(WebAssembly — Curve25519 / XSalsa20-Poly1305)
|
||||
WebRTC API — ビデオ/音声通話
|
||||
PWA: manifest.json + Service Worker
|
||||
|
||||
Cryptographic Layer
|
||||
Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message
|
||||
Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB
|
||||
All private keys stored on-device only — never sent to the server
|
||||
暗号化レイヤー
|
||||
ステートレス ECDH + XSalsa20-Poly1305 — メッセージごとの一時 ECDH 鍵ペア
|
||||
秘密鍵4層永続化: メモリ → localStorage → sessionStorage → IndexedDB
|
||||
秘密鍵はデバイスにのみ保存、サーバーに送信されることはありません
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
## クイックスタート
|
||||
|
||||
### Option 0: Zeabur One-Click Cloud Deploy
|
||||
### 方法0:Zeabur ワンクリッククラウドデプロイ
|
||||
|
||||
[](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
|
||||
|
||||
> [!NOTE]
|
||||
> One manual step is required after the template deploys, otherwise login/register won't work:
|
||||
> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`)
|
||||
> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above
|
||||
> 3. Restart the client service
|
||||
> テンプレートのデプロイ後、1つの手動手順が必要です。これを行わないとログイン/登録が機能しません:
|
||||
> 1. Zeabur コンソール → **server サービス** → 環境変数 → `ZEABUR_WEB_URL` の値をコピー(例:`http://10.43.x.x:3000`)
|
||||
> 2. **client サービス** → 環境変数 → 変数 `SERVER_URL` を追加 = 上記でコピーした値
|
||||
> 3. client サービスを再起動
|
||||
|
||||
**Known notes:**
|
||||
- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed
|
||||
- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient)
|
||||
- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service
|
||||
- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run:
|
||||
**既知の注意事項:**
|
||||
- 初回起動時、サーバーは自動的にすべてのデータベーステーブルを作成します(`CREATE TABLE IF NOT EXISTS`)— SQL の手動インポートは不要
|
||||
- Redis はクラスター内でパスワードなしで動作します
|
||||
- MySQL アクセスが拒否された場合、server サービスの `DB_PASS` を MySQL サービスの `MYSQL_ROOT_PASSWORD` に手動設定してください
|
||||
- サービスコンテナの**内部IP**を取得するには、Zeabur コンソールで該当サービスのターミナルを開き、以下を実行:
|
||||
```bash
|
||||
hostname -i
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option 1: Docker Compose (Recommended — no local build needed)
|
||||
### 方法1:Docker Compose(推奨 — ローカルビルド不要)
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
# リポジトリをクローン
|
||||
git clone <repo-url> && cd paperphone
|
||||
|
||||
# Copy and edit environment variables
|
||||
# 環境変数をコピーして編集
|
||||
cp server/.env.example server/.env
|
||||
# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc.
|
||||
# DB_PASS / JWT_SECRET / R2_* などを編集
|
||||
|
||||
# Pull images and start everything
|
||||
# イメージを取得して起動
|
||||
docker compose up -d
|
||||
|
||||
# Check service status
|
||||
# サービスステータスを確認
|
||||
docker compose ps
|
||||
|
||||
# Open in browser
|
||||
# ブラウザで開く
|
||||
open http://localhost
|
||||
```
|
||||
|
||||
> Pre-built images on Docker Hub:
|
||||
> Docker Hub のビルド済みイメージ:
|
||||
> - `facilisvelox/paperphone-client:latest`
|
||||
> - `facilisvelox/paperphone-server:latest`
|
||||
>
|
||||
> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required.
|
||||
> **注意**:サーバーは初回起動時に自動的にデータベーススキーマを初期化します — SQL の手動インポートは不要です。
|
||||
|
||||
### Option 2: Manual Local Start
|
||||
### 方法2:ローカル手動起動
|
||||
|
||||
#### 1. Prepare the environment
|
||||
#### 1. 環境を準備
|
||||
|
||||
```bash
|
||||
# Copy and edit environment variables
|
||||
# 環境変数をコピーして編集
|
||||
cp server/.env.example server/.env
|
||||
# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
|
||||
# DB_HOST / DB_PASS / REDIS_HOST / R2_* などを入力
|
||||
|
||||
# Note: the server auto-runs schema.sql on first startup
|
||||
# 注:サーバーは初回起動時に自動で schema.sql を実行します
|
||||
```
|
||||
|
||||
#### 2. Start the backend
|
||||
#### 2. バックエンドを起動
|
||||
|
||||
```bash
|
||||
cd server
|
||||
@@ -123,7 +124,7 @@ npm install
|
||||
npm run dev # → http://localhost:3000
|
||||
```
|
||||
|
||||
#### 3. Start the frontend
|
||||
#### 3. フロントエンドを起動
|
||||
|
||||
```bash
|
||||
npx serve client -p 8080
|
||||
@@ -132,54 +133,54 @@ npx serve client -p 8080
|
||||
|
||||
---
|
||||
|
||||
## Video Call Configuration
|
||||
## ビデオ通話の設定
|
||||
|
||||
Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal.
|
||||
ビデオ通話と音声通話は WebRTC P2P を使用し、同一 LAN 内ではすぐに使えます。異なるネットワーク間の通話には、NAT トラバーサル用の TURN サーバーが必要です。
|
||||
|
||||
### Using Cloudflare TURN (Recommended)
|
||||
### Cloudflare TURN の使用(推奨)
|
||||
|
||||
1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App
|
||||
2. Copy the **App ID** and **App Secret** (Token Key)
|
||||
3. Add them to `server/.env`:
|
||||
1. [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → App を作成
|
||||
2. **App ID** と **App Secret**(Token Key)をコピー
|
||||
3. `server/.env` に追加:
|
||||
|
||||
```env
|
||||
CF_CALLS_APP_ID=your_app_id_here
|
||||
CF_CALLS_APP_SECRET=your_app_secret_here
|
||||
```
|
||||
|
||||
4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s)
|
||||
4. バックエンドを再起動 — TURN クレデンシャルは通話セッションごとに自動取得されます(TTL: 86,400秒)
|
||||
|
||||
> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration.
|
||||
> **未設定時**:STUN のみにフォールバックします(Google + Cloudflare パブリック STUN)。LAN 内の通話は追加設定なしで動作します。
|
||||
|
||||
### Call Types
|
||||
### 通話タイプ
|
||||
|
||||
| Type | Transport | Recommended for |
|
||||
|------|-----------|-----------------|
|
||||
| 1:1 Video Call | WebRTC P2P + TURN | All scenarios |
|
||||
| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios |
|
||||
| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants |
|
||||
| タイプ | 通信方式 | 推奨用途 |
|
||||
|--------|----------|----------|
|
||||
| 1:1 ビデオ通話 | WebRTC P2P + TURN | すべてのシナリオ |
|
||||
| 1:1 音声通話 | WebRTC P2P + TURN | すべてのシナリオ |
|
||||
| グループ通話 | WebRTC Mesh(フルメッシュ) | 最大6人 |
|
||||
|
||||
---
|
||||
|
||||
## Push Notification Configuration
|
||||
## プッシュ通知の設定
|
||||
|
||||
Offline message notifications are delivered through **two channels** for maximum delivery rate:
|
||||
オフラインメッセージ通知は**2つのチャネル**で配信され、配信率を最大化します:
|
||||
|
||||
| Channel | Platforms | Configuration |
|
||||
|---------|-----------|---------------|
|
||||
| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys |
|
||||
| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key |
|
||||
| チャネル | 対応プラットフォーム | 設定 |
|
||||
|----------|----------------------|------|
|
||||
| Web Push (VAPID) | ブラウザ (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID キー |
|
||||
| OneSignal | Median.co 経由のネイティブ Android/iOS アプリ | OneSignal App ID + REST Key |
|
||||
|
||||
### Configuring Web Push
|
||||
### Web Push の設定
|
||||
|
||||
1. Generate VAPID keys (one-time):
|
||||
1. VAPID キーを生成(1回のみ):
|
||||
|
||||
```bash
|
||||
cd server
|
||||
npx web-push generate-vapid-keys
|
||||
```
|
||||
|
||||
2. Add to `server/.env`:
|
||||
2. `server/.env` に追加:
|
||||
|
||||
```env
|
||||
VAPID_PUBLIC_KEY=your_public_key_here
|
||||
@@ -187,37 +188,37 @@ VAPID_PRIVATE_KEY=your_private_key_here
|
||||
VAPID_SUBJECT=mailto:admin@your-domain.com
|
||||
```
|
||||
|
||||
3. Restart the server — users can enable notifications from the Settings page
|
||||
3. サーバーを再起動 — ユーザーは設定ページから通知を有効にできます
|
||||
|
||||
> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported.
|
||||
> **iOS ユーザー**は先に「ホーム画面に追加」(PWA)を行う必要があり、iOS 16.4以上のみサポートされます。
|
||||
|
||||
### Configuring OneSignal (Median.co Native Apps)
|
||||
### OneSignal の設定(Median.co ネイティブアプリ)
|
||||
|
||||
1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase
|
||||
2. Enable OneSignal in Median.co and enter the App ID
|
||||
3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`:
|
||||
1. [OneSignal Dashboard](https://onesignal.com) でアプリを作成し、Firebase を設定
|
||||
2. Median.co で OneSignal を有効にし、App ID を入力
|
||||
3. OneSignal の **App ID** と **REST API Key** を `server/.env` に追加:
|
||||
|
||||
```env
|
||||
ONESIGNAL_APP_ID=your_onesignal_app_id
|
||||
ONESIGNAL_REST_KEY=your_onesignal_rest_api_key
|
||||
```
|
||||
|
||||
> **When not configured**: push notifications are silently disabled — all other features work normally.
|
||||
> **未設定時**:プッシュ通知は自動的に無効化されます — 他の機能には影響しません。
|
||||
|
||||
---
|
||||
|
||||
## iOS — Permanent No-Cert Deployment
|
||||
## iOS — 証明書不要の永続デプロイ
|
||||
|
||||
1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs)
|
||||
2. Open `https://your.domain.com` in **Safari**
|
||||
3. Tap the Share button ⬆️ at the bottom of the screen
|
||||
4. Select **Add to Home Screen** → **Add**
|
||||
1. HTTPS ドメインのサーバーにデプロイ(WebRTC と Web Crypto API には HTTPS が必要)
|
||||
2. **Safari** で `https://your.domain.com` を開く
|
||||
3. 画面下部の共有ボタン ⬆️ をタップ
|
||||
4. **ホーム画面に追加** → **追加** を選択
|
||||
|
||||
The app behaves identically to a native app — no Apple enterprise certificate required, no expiry.
|
||||
ネイティブアプリと同様に動作します — Apple エンタープライズ証明書不要、期限なし。
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment (Nginx)
|
||||
## 本番デプロイ(Nginx)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
@@ -227,7 +228,7 @@ server {
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
# Frontend static files
|
||||
# フロントエンド静的ファイル
|
||||
location / {
|
||||
root /path/to/paperphone/client;
|
||||
try_files $uri /index.html;
|
||||
@@ -239,7 +240,7 @@ server {
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# WebSocket (messaging + call signalling)
|
||||
# WebSocket(メッセージング+通話シグナリング)
|
||||
location /ws {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_http_version 1.1;
|
||||
@@ -252,125 +253,69 @@ server {
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
## データベーススキーマ
|
||||
|
||||
11テーブル、サーバー初回起動時に自動作成(`CREATE TABLE IF NOT EXISTS`):
|
||||
|
||||
| テーブル | 用途 |
|
||||
|----------|------|
|
||||
| `users` | ユーザー情報 + ECDH/OPK 公開鍵 |
|
||||
| `prekeys` | X3DH ワンタイムプリキープール |
|
||||
| `friends` | 友達関係(pending / accepted / blocked) |
|
||||
| `groups` / `group_members` | グループチャット+メンバー(通知オフ状態含む) |
|
||||
| `messages` | 暗号化メッセージ(オフラインバッファ、配信後削除可能) |
|
||||
| `moments` | ソーシャル投稿(テキスト ≤ 1024文字) |
|
||||
| `moment_images` | 投稿画像(1投稿最大9枚) |
|
||||
| `moment_likes` | いいね(ユーザーごと投稿ごとにユニーク) |
|
||||
| `moment_comments` | コメント(≤ 512文字/件) |
|
||||
| `push_subscriptions` | Web Push サブスクリプション(VAPID) |
|
||||
| `onesignal_players` | OneSignal デバイス登録(Median.co) |
|
||||
|
||||
---
|
||||
|
||||
## セキュリティモデル
|
||||
|
||||
```
|
||||
paperphone/
|
||||
├── docker-compose.yml
|
||||
├── server/
|
||||
│ ├── .env # Environment variables (incl. Cloudflare TURN keys)
|
||||
│ └── src/
|
||||
│ ├── app.js # Express application
|
||||
│ ├── routes/
|
||||
│ │ ├── auth.js # Register / Login (incl. X3DH public key upload)
|
||||
│ │ ├── users.js # User search / Prekey bundle download
|
||||
│ │ ├── friends.js # Friend requests / Accept (incl. offline push)
|
||||
│ │ ├── groups.js # Group management
|
||||
│ │ ├── messages.js # Historical messages (paginated ciphertext)
|
||||
│ │ ├── upload.js # Cloudflare R2 file upload
|
||||
│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set)
|
||||
│ │ ├── moments.js # Moments feed (posts / likes / comments)
|
||||
│ │ ├── calls.js # TURN credential issuance
|
||||
│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal)
|
||||
│ ├── services/
|
||||
│ │ ├── push.js # Web Push VAPID service
|
||||
│ │ └── onesignal.js # OneSignal REST API service
|
||||
│ └── ws/
|
||||
│ └── wsServer.js # WebSocket router (messages + call signalling + offline push)
|
||||
│
|
||||
└── client/
|
||||
├── index.html # SPA entry + PWA meta + Median push bridge
|
||||
├── manifest.json # PWA manifest
|
||||
├── sw.js # Service Worker (offline cache + push notifications)
|
||||
└── src/
|
||||
├── style.css # Premium design system (dark/light, glassmorphism)
|
||||
├── app.js # Router + global state + incoming call listener
|
||||
├── api.js # HTTP client
|
||||
├── socket.js # WebSocket client (auto-reconnect)
|
||||
├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es)
|
||||
├── services/
|
||||
│ ├── webrtc.js # WebRTC manager — CallManager class
|
||||
│ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge)
|
||||
├── crypto/
|
||||
│ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768
|
||||
│ └── keystore.js # IndexedDB private key store
|
||||
├── pages/
|
||||
│ ├── login.js # Login / Register (key generation, language picker)
|
||||
│ ├── chats.js # Chat list
|
||||
│ ├── chat.js # Chat window (E2E encryption, call buttons)
|
||||
│ ├── groups.js # Group list (create group, search)
|
||||
│ ├── groupInfo.js # Group info (member management, DND, leave/disband)
|
||||
│ ├── contacts.js # Contacts (friend requests, online status)
|
||||
│ ├── discover.js # Discover page
|
||||
│ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA)
|
||||
│ └── call.js # Call UI (incoming / active / multi-party video)
|
||||
登録時:
|
||||
デバイスが IK(アイデンティティキー)+ SPK(署名付きプリキー)+ 10× OPK(ワンタイムプリキー)を生成
|
||||
公開鍵はアップロード、秘密鍵はデバイスに保存(4層永続化)
|
||||
|
||||
メッセージ送信時:
|
||||
送信者が受信者の IK 公開鍵を取得
|
||||
一時 ECDH 鍵ペアを生成(メッセージごとに新しいペア)
|
||||
X25519 ECDH → 共有秘密 → XSalsa20-Poly1305 暗号化
|
||||
一時公開鍵はメッセージヘッダーで送信、使用後に破棄
|
||||
|
||||
サーバーが見るもの:
|
||||
✅ 暗号文ブロブ+ルーティングメタデータ(送受信者UUID、タイムスタンプ)
|
||||
❌ 平文 / 秘密鍵 / 一時鍵 / 通話内容
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
## 環境変数リファレンス
|
||||
|
||||
11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`):
|
||||
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `users` | User profiles + ECDH/OPK public keys |
|
||||
| `prekeys` | X3DH one-time prekey pool |
|
||||
| `friends` | Friendship relationships (pending / accepted / blocked) |
|
||||
| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) |
|
||||
| `messages` | Encrypted payloads (offline buffer, deletable after delivery) |
|
||||
| `moments` | Social posts (text ≤ 1024 chars) |
|
||||
| `moment_images` | Post images (up to 9 per post) |
|
||||
| `moment_likes` | Likes (unique per user per post) |
|
||||
| `moment_comments` | Comments (≤ 512 chars each) |
|
||||
| `push_subscriptions` | Web Push subscriptions (VAPID) |
|
||||
| `onesignal_players` | OneSignal device registrations (Median.co) |
|
||||
| 変数 | 説明 | デフォルト |
|
||||
|------|------|------------|
|
||||
| `PORT` | サーバーポート | `3000` |
|
||||
| `JWT_SECRET` | JWT 署名キー(**本番環境では必ず変更**) | dev_secret |
|
||||
| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL 接続設定 | — |
|
||||
| `REDIS_HOST` / `REDIS_PASS` | Redis 接続設定 | — |
|
||||
| `R2_ACCOUNT_ID` | Cloudflare アカウント ID | — |
|
||||
| `R2_ACCESS_KEY_ID` | R2 API トークンのアクセスキー | — |
|
||||
| `R2_SECRET_ACCESS_KEY` | R2 API トークンのシークレットキー | — |
|
||||
| `R2_BUCKET` | R2 バケット名 | — |
|
||||
| `R2_PUBLIC_URL` | R2 公開 URL(任意)— CDN 直リンクを有効化 | — |
|
||||
| `CF_CALLS_APP_ID` | Cloudflare Calls App ID(任意) | — |
|
||||
| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret(任意) | — |
|
||||
| `VAPID_PUBLIC_KEY` | Web Push VAPID 公開鍵(任意) | — |
|
||||
| `VAPID_PRIVATE_KEY` | Web Push VAPID 秘密鍵(任意) | — |
|
||||
| `VAPID_SUBJECT` | VAPID 連絡先メール(任意) | `mailto:admin@paperphone.app` |
|
||||
| `ONESIGNAL_APP_ID` | OneSignal App ID(任意、Median.co用) | — |
|
||||
| `ONESIGNAL_REST_KEY` | OneSignal REST API Key(任意) | — |
|
||||
|
||||
---
|
||||
|
||||
## Security Model
|
||||
|
||||
```
|
||||
On Registration:
|
||||
Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys)
|
||||
Public keys are uploaded; private keys stay on-device (four-tier persistence)
|
||||
|
||||
On Each Message:
|
||||
Sender fetches recipient's IK public key
|
||||
Generates a fresh ephemeral ECDH keypair (per-message)
|
||||
X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt
|
||||
Ephemeral public key sent in message header; destroyed after use
|
||||
|
||||
What the Server Sees:
|
||||
✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps)
|
||||
❌ Plaintext / private keys / ephemeral keys / call content
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PORT` | Server port | `3000` |
|
||||
| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret |
|
||||
| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — |
|
||||
| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — |
|
||||
| `R2_ACCOUNT_ID` | Cloudflare account ID | — |
|
||||
| `R2_ACCESS_KEY_ID` | R2 API token access key | — |
|
||||
| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — |
|
||||
| `R2_BUCKET` | R2 bucket name | — |
|
||||
| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — |
|
||||
| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — |
|
||||
| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — |
|
||||
| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — |
|
||||
| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — |
|
||||
| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` |
|
||||
| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — |
|
||||
| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — |
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
## ライセンス
|
||||
|
||||
MIT © PaperPhone Contributors
|
||||
|
||||
355
README_KO.md
355
README_KO.md
@@ -10,112 +10,113 @@ WeChat 스타일의 종단간 암호화 인스턴트 메시징 앱. 무상태 EC
|
||||
|
||||
---
|
||||
<img width=30% height=30% src="https://raw.githubusercontent.com/619dev/PaperPhone/main/ui.jpg" alt="ui">
|
||||
## Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy |
|
||||
| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device |
|
||||
| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal |
|
||||
| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management |
|
||||
| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups |
|
||||
| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline |
|
||||
| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch |
|
||||
| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing |
|
||||
| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators |
|
||||
| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control |
|
||||
| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag |
|
||||
| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL |
|
||||
| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready |
|
||||
## 기능
|
||||
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| 🔐 종단간 암호화 | 무상태 ECDH + XSalsa20-Poly1305 — 메시지별 임시 키, 전방 비밀성 |
|
||||
| 🗝️ 제로 지식 서버 | 서버는 암호문만 저장, 개인 키는 기기를 떠나지 않음 |
|
||||
| 📹 영상/음성 통화 | WebRTC P2P (1:1) + Mesh (그룹), Cloudflare TURN을 통한 NAT 트래버설 |
|
||||
| 👥 그룹 채팅 | 최대 2000명, 일반 텍스트 메시지 (비암호화), 방해 금지 모드, 멤버 관리 |
|
||||
| ⏱️ 메시지 자동 삭제 | 5단계 (안함/1일/3일/1주/1개월), DM에서 양쪽 설정 가능, 그룹은 방장만 |
|
||||
| 🔔 알림 | Web Push (VAPID) + OneSignal 이중 채널 — 오프라인에서도 알림 수신 |
|
||||
| 🌐 다국어 | 중국어·영어·일본어·한국어·프랑스어·독일어·러시아어·스페인어 (자동 감지 + 수동 전환) |
|
||||
| 📱 iOS — 기업 인증서 불필요 | Safari "홈 화면에 추가"를 통한 PWA, Apple 서명 없이 영구 작동 |
|
||||
| 💬 풍부한 메시징 | 텍스트, 이미지, 음성 메시지, 이모지 패널 (64종), 읽음 확인 |
|
||||
| 🌐 모먼트 | 텍스트 + 최대 9장 사진, 좋아요 (친구 아바타 표시), 댓글, 태그 기반 공개 범위 제어 |
|
||||
| 🏷️ 친구 태그 | 친구에게 여러 태그 할당 (12색 프리셋), 태그별 연락처 필터링 |
|
||||
| 🗂️ R2 오브젝트 스토리지 | Cloudflare R2로 이미지/음성 파일 저장 — 선택적 공개 CDN URL |
|
||||
| 🏗️ 셀프 호스팅 가능 | Docker Compose 원커맨드 배포, Node.js + Redis 멀티 노드 지원 |
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
## 기술 스택
|
||||
|
||||
```
|
||||
Backend (server/)
|
||||
백엔드 (server/)
|
||||
Node.js 20 + Express + ws
|
||||
MySQL 8.0 — users, messages (persisted ciphertext)
|
||||
Redis — online presence + cross-node routing
|
||||
Cloudflare R2 — image/voice file storage (S3-compatible API)
|
||||
JWT + bcrypt authentication
|
||||
MySQL 8.0 — 사용자, 메시지 영속화 (암호문)
|
||||
Redis — 온라인 상태 + 크로스 노드 라우팅
|
||||
Cloudflare R2 — 이미지/음성 파일 저장소 (S3 호환 API)
|
||||
JWT + bcrypt 인증
|
||||
|
||||
Frontend (client/)
|
||||
Native HTML + Vanilla JS (ESM, no bundler required)
|
||||
프론트엔드 (client/)
|
||||
네이티브 HTML + Vanilla JS (ESM, 번들러 불필요)
|
||||
libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305)
|
||||
WebRTC API — video / voice calls
|
||||
WebRTC API — 영상/음성 통화
|
||||
PWA: manifest.json + Service Worker
|
||||
|
||||
Cryptographic Layer
|
||||
Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message
|
||||
Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB
|
||||
All private keys stored on-device only — never sent to the server
|
||||
암호화 레이어
|
||||
무상태 ECDH + XSalsa20-Poly1305 — 메시지별 임시 ECDH 키페어
|
||||
개인 키 4단계 영속화: 메모리 → localStorage → sessionStorage → IndexedDB
|
||||
모든 개인 키는 기기에만 저장 — 절대 서버로 전송되지 않음
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
## 빠른 시작
|
||||
|
||||
### Option 0: Zeabur One-Click Cloud Deploy
|
||||
### 방법 0: Zeabur 원클릭 클라우드 배포
|
||||
|
||||
[](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
|
||||
|
||||
> [!NOTE]
|
||||
> One manual step is required after the template deploys, otherwise login/register won't work:
|
||||
> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`)
|
||||
> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above
|
||||
> 3. Restart the client service
|
||||
> 템플릿 배포 후 수동 단계 하나가 필요합니다. 이 작업을 하지 않으면 로그인/회원가입이 작동하지 않습니다:
|
||||
> 1. Zeabur 콘솔 → **server 서비스** → 환경 변수 → `ZEABUR_WEB_URL` 값 복사 (예: `http://10.43.x.x:3000`)
|
||||
> 2. **client 서비스** → 환경 변수 → 변수 `SERVER_URL` 추가 = 위에서 복사한 값
|
||||
> 3. client 서비스 재시작
|
||||
|
||||
**Known notes:**
|
||||
- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed
|
||||
- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient)
|
||||
- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service
|
||||
- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run:
|
||||
**알려진 참고 사항:**
|
||||
- 첫 시작 시 서버가 자동으로 모든 데이터베이스 테이블을 생성합니다 (`CREATE TABLE IF NOT EXISTS`) — SQL 수동 임포트 불필요
|
||||
- Redis는 클러스터 내에서 비밀번호 없이 작동합니다
|
||||
- MySQL 접근이 거부되면 server 서비스의 `DB_PASS`를 MySQL 서비스의 `MYSQL_ROOT_PASSWORD` 값으로 수동 설정하세요
|
||||
- 서비스 컨테이너의 **내부 IP**를 확인하려면 Zeabur 콘솔에서 해당 서비스의 터미널을 열고 실행:
|
||||
```bash
|
||||
hostname -i
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option 1: Docker Compose (Recommended — no local build needed)
|
||||
### 방법 1: Docker Compose (권장 — 로컬 빌드 불필요)
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
# 저장소 클론
|
||||
git clone <repo-url> && cd paperphone
|
||||
|
||||
# Copy and edit environment variables
|
||||
# 환경 변수 복사 및 편집
|
||||
cp server/.env.example server/.env
|
||||
# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc.
|
||||
# DB_PASS / JWT_SECRET / R2_* 등 입력
|
||||
|
||||
# Pull images and start everything
|
||||
# 이미지 풀 및 시작
|
||||
docker compose up -d
|
||||
|
||||
# Check service status
|
||||
# 서비스 상태 확인
|
||||
docker compose ps
|
||||
|
||||
# Open in browser
|
||||
# 브라우저에서 열기
|
||||
open http://localhost
|
||||
```
|
||||
|
||||
> Pre-built images on Docker Hub:
|
||||
> Docker Hub의 사전 빌드 이미지:
|
||||
> - `facilisvelox/paperphone-client:latest`
|
||||
> - `facilisvelox/paperphone-server:latest`
|
||||
>
|
||||
> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required.
|
||||
> **참고**: 서버는 첫 시작 시 자동으로 데이터베이스 스키마를 초기화합니다 — SQL 수동 임포트가 필요하지 않습니다.
|
||||
|
||||
### Option 2: Manual Local Start
|
||||
### 방법 2: 로컬 수동 시작
|
||||
|
||||
#### 1. Prepare the environment
|
||||
#### 1. 환경 준비
|
||||
|
||||
```bash
|
||||
# Copy and edit environment variables
|
||||
# 환경 변수 복사 및 편집
|
||||
cp server/.env.example server/.env
|
||||
# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
|
||||
# DB_HOST / DB_PASS / REDIS_HOST / R2_* 등 입력
|
||||
|
||||
# Note: the server auto-runs schema.sql on first startup
|
||||
# 참고: 서버는 첫 시작 시 자동으로 schema.sql을 실행합니다
|
||||
```
|
||||
|
||||
#### 2. Start the backend
|
||||
#### 2. 백엔드 시작
|
||||
|
||||
```bash
|
||||
cd server
|
||||
@@ -123,7 +124,7 @@ npm install
|
||||
npm run dev # → http://localhost:3000
|
||||
```
|
||||
|
||||
#### 3. Start the frontend
|
||||
#### 3. 프론트엔드 시작
|
||||
|
||||
```bash
|
||||
npx serve client -p 8080
|
||||
@@ -132,54 +133,54 @@ npx serve client -p 8080
|
||||
|
||||
---
|
||||
|
||||
## Video Call Configuration
|
||||
## 영상 통화 설정
|
||||
|
||||
Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal.
|
||||
영상 및 음성 통화는 WebRTC P2P를 사용하며 동일 LAN에서 바로 사용할 수 있습니다. 다른 네트워크 간 통화에는 NAT 트래버설을 위한 TURN 서버가 필요합니다.
|
||||
|
||||
### Using Cloudflare TURN (Recommended)
|
||||
### Cloudflare TURN 사용 (권장)
|
||||
|
||||
1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App
|
||||
2. Copy the **App ID** and **App Secret** (Token Key)
|
||||
3. Add them to `server/.env`:
|
||||
1. [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → 앱 생성
|
||||
2. **App ID**와 **App Secret** (토큰 키) 복사
|
||||
3. `server/.env`에 추가:
|
||||
|
||||
```env
|
||||
CF_CALLS_APP_ID=your_app_id_here
|
||||
CF_CALLS_APP_SECRET=your_app_secret_here
|
||||
```
|
||||
|
||||
4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s)
|
||||
4. 백엔드 재시작 — TURN 자격 증명은 통화 세션마다 자동 발급됩니다 (TTL: 86,400초)
|
||||
|
||||
> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration.
|
||||
> **미설정 시**: STUN 전용으로 폴백합니다 (Google + Cloudflare 공개 STUN). LAN 통화는 추가 설정 없이 작동합니다.
|
||||
|
||||
### Call Types
|
||||
### 통화 유형
|
||||
|
||||
| Type | Transport | Recommended for |
|
||||
|------|-----------|-----------------|
|
||||
| 1:1 Video Call | WebRTC P2P + TURN | All scenarios |
|
||||
| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios |
|
||||
| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants |
|
||||
| 유형 | 전송 방식 | 권장 사용 |
|
||||
|------|-----------|-----------|
|
||||
| 1:1 영상 통화 | WebRTC P2P + TURN | 모든 시나리오 |
|
||||
| 1:1 음성 통화 | WebRTC P2P + TURN | 모든 시나리오 |
|
||||
| 그룹 영상/음성 | WebRTC Mesh (풀 메시) | 최대 6명 |
|
||||
|
||||
---
|
||||
|
||||
## Push Notification Configuration
|
||||
## 푸시 알림 설정
|
||||
|
||||
Offline message notifications are delivered through **two channels** for maximum delivery rate:
|
||||
오프라인 메시지 알림은 **두 채널**을 통해 배달되어 최대 전달률을 보장합니다:
|
||||
|
||||
| Channel | Platforms | Configuration |
|
||||
|---------|-----------|---------------|
|
||||
| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys |
|
||||
| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key |
|
||||
| 채널 | 플랫폼 | 설정 |
|
||||
|------|--------|------|
|
||||
| Web Push (VAPID) | 브라우저 (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID 키 |
|
||||
| OneSignal | Median.co 경유 네이티브 Android/iOS 앱 | OneSignal App ID + REST Key |
|
||||
|
||||
### Configuring Web Push
|
||||
### Web Push 설정
|
||||
|
||||
1. Generate VAPID keys (one-time):
|
||||
1. VAPID 키 생성 (1회만):
|
||||
|
||||
```bash
|
||||
cd server
|
||||
npx web-push generate-vapid-keys
|
||||
```
|
||||
|
||||
2. Add to `server/.env`:
|
||||
2. `server/.env`에 추가:
|
||||
|
||||
```env
|
||||
VAPID_PUBLIC_KEY=your_public_key_here
|
||||
@@ -187,37 +188,37 @@ VAPID_PRIVATE_KEY=your_private_key_here
|
||||
VAPID_SUBJECT=mailto:admin@your-domain.com
|
||||
```
|
||||
|
||||
3. Restart the server — users can enable notifications from the Settings page
|
||||
3. 서버 재시작 — 사용자가 설정 페이지에서 알림을 활성화할 수 있습니다
|
||||
|
||||
> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported.
|
||||
> **iOS 사용자**는 먼저 "홈 화면에 추가" (PWA)를 해야 하며, iOS 16.4 이상만 지원됩니다.
|
||||
|
||||
### Configuring OneSignal (Median.co Native Apps)
|
||||
### OneSignal 설정 (Median.co 네이티브 앱)
|
||||
|
||||
1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase
|
||||
2. Enable OneSignal in Median.co and enter the App ID
|
||||
3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`:
|
||||
1. [OneSignal Dashboard](https://onesignal.com)에서 앱 생성 및 Firebase 설정
|
||||
2. Median.co에서 OneSignal 활성화 후 App ID 입력
|
||||
3. OneSignal **App ID**와 **REST API Key**를 `server/.env`에 추가:
|
||||
|
||||
```env
|
||||
ONESIGNAL_APP_ID=your_onesignal_app_id
|
||||
ONESIGNAL_REST_KEY=your_onesignal_rest_api_key
|
||||
```
|
||||
|
||||
> **When not configured**: push notifications are silently disabled — all other features work normally.
|
||||
> **미설정 시**: 푸시 알림이 자동으로 비활성화됩니다 — 다른 기능에는 영향 없습니다.
|
||||
|
||||
---
|
||||
|
||||
## iOS — Permanent No-Cert Deployment
|
||||
## iOS — 인증서 없는 영구 배포
|
||||
|
||||
1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs)
|
||||
2. Open `https://your.domain.com` in **Safari**
|
||||
3. Tap the Share button ⬆️ at the bottom of the screen
|
||||
4. Select **Add to Home Screen** → **Add**
|
||||
1. HTTPS 도메인 서버에 배포 (WebRTC 및 Web Crypto API에 HTTPS 필요)
|
||||
2. **Safari**에서 `https://your.domain.com` 열기
|
||||
3. 화면 하단의 공유 버튼 ⬆️ 탭
|
||||
4. **홈 화면에 추가** → **추가** 선택
|
||||
|
||||
The app behaves identically to a native app — no Apple enterprise certificate required, no expiry.
|
||||
네이티브 앱과 동일하게 작동합니다 — Apple 기업 인증서 불필요, 만료 없음.
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment (Nginx)
|
||||
## 프로덕션 배포 (Nginx)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
@@ -227,7 +228,7 @@ server {
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
# Frontend static files
|
||||
# 프론트엔드 정적 파일
|
||||
location / {
|
||||
root /path/to/paperphone/client;
|
||||
try_files $uri /index.html;
|
||||
@@ -239,7 +240,7 @@ server {
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# WebSocket (messaging + call signalling)
|
||||
# WebSocket (메시징 + 통화 시그널링)
|
||||
location /ws {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_http_version 1.1;
|
||||
@@ -252,125 +253,69 @@ server {
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
## 데이터베이스 스키마
|
||||
|
||||
11개 테이블, 서버 첫 시작 시 자동 생성 (`CREATE TABLE IF NOT EXISTS`):
|
||||
|
||||
| 테이블 | 용도 |
|
||||
|--------|------|
|
||||
| `users` | 사용자 프로필 + ECDH/OPK 공개 키 |
|
||||
| `prekeys` | X3DH 원타임 프리키 풀 |
|
||||
| `friends` | 친구 관계 (pending / accepted / blocked) |
|
||||
| `groups` / `group_members` | 그룹 채팅 + 멤버 (알림 끄기 상태 포함) |
|
||||
| `messages` | 암호화된 메시지 (오프라인 버퍼, 전달 후 삭제 가능) |
|
||||
| `moments` | 소셜 게시물 (텍스트 ≤ 1024자) |
|
||||
| `moment_images` | 게시물 이미지 (게시물당 최대 9개) |
|
||||
| `moment_likes` | 좋아요 (사용자당 게시물당 고유) |
|
||||
| `moment_comments` | 댓글 (≤ 512자) |
|
||||
| `push_subscriptions` | Web Push 구독 (VAPID) |
|
||||
| `onesignal_players` | OneSignal 기기 등록 (Median.co) |
|
||||
|
||||
---
|
||||
|
||||
## 보안 모델
|
||||
|
||||
```
|
||||
paperphone/
|
||||
├── docker-compose.yml
|
||||
├── server/
|
||||
│ ├── .env # Environment variables (incl. Cloudflare TURN keys)
|
||||
│ └── src/
|
||||
│ ├── app.js # Express application
|
||||
│ ├── routes/
|
||||
│ │ ├── auth.js # Register / Login (incl. X3DH public key upload)
|
||||
│ │ ├── users.js # User search / Prekey bundle download
|
||||
│ │ ├── friends.js # Friend requests / Accept (incl. offline push)
|
||||
│ │ ├── groups.js # Group management
|
||||
│ │ ├── messages.js # Historical messages (paginated ciphertext)
|
||||
│ │ ├── upload.js # Cloudflare R2 file upload
|
||||
│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set)
|
||||
│ │ ├── moments.js # Moments feed (posts / likes / comments)
|
||||
│ │ ├── calls.js # TURN credential issuance
|
||||
│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal)
|
||||
│ ├── services/
|
||||
│ │ ├── push.js # Web Push VAPID service
|
||||
│ │ └── onesignal.js # OneSignal REST API service
|
||||
│ └── ws/
|
||||
│ └── wsServer.js # WebSocket router (messages + call signalling + offline push)
|
||||
│
|
||||
└── client/
|
||||
├── index.html # SPA entry + PWA meta + Median push bridge
|
||||
├── manifest.json # PWA manifest
|
||||
├── sw.js # Service Worker (offline cache + push notifications)
|
||||
└── src/
|
||||
├── style.css # Premium design system (dark/light, glassmorphism)
|
||||
├── app.js # Router + global state + incoming call listener
|
||||
├── api.js # HTTP client
|
||||
├── socket.js # WebSocket client (auto-reconnect)
|
||||
├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es)
|
||||
├── services/
|
||||
│ ├── webrtc.js # WebRTC manager — CallManager class
|
||||
│ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge)
|
||||
├── crypto/
|
||||
│ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768
|
||||
│ └── keystore.js # IndexedDB private key store
|
||||
├── pages/
|
||||
│ ├── login.js # Login / Register (key generation, language picker)
|
||||
│ ├── chats.js # Chat list
|
||||
│ ├── chat.js # Chat window (E2E encryption, call buttons)
|
||||
│ ├── groups.js # Group list (create group, search)
|
||||
│ ├── groupInfo.js # Group info (member management, DND, leave/disband)
|
||||
│ ├── contacts.js # Contacts (friend requests, online status)
|
||||
│ ├── discover.js # Discover page
|
||||
│ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA)
|
||||
│ └── call.js # Call UI (incoming / active / multi-party video)
|
||||
등록 시:
|
||||
기기가 IK (아이덴티티 키) + SPK (서명 프리키) + 10× OPK (원타임 프리키) 생성
|
||||
공개 키는 업로드, 개인 키는 기기에 보관 (4단계 영속화)
|
||||
|
||||
메시지 전송 시:
|
||||
발신자가 수신자의 IK 공개 키를 가져옴
|
||||
임시 ECDH 키페어 생성 (메시지마다 새로운 페어)
|
||||
X25519 ECDH → 공유 비밀 → XSalsa20-Poly1305 암호화
|
||||
임시 공개 키는 메시지 헤더로 전송, 사용 후 파기
|
||||
|
||||
서버가 보는 것:
|
||||
✅ 암호문 블롭 + 라우팅 메타데이터 (발신자/수신자 UUID, 타임스탬프)
|
||||
❌ 평문 / 개인 키 / 임시 키 / 통화 내용
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
## 환경 변수
|
||||
|
||||
11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`):
|
||||
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `users` | User profiles + ECDH/OPK public keys |
|
||||
| `prekeys` | X3DH one-time prekey pool |
|
||||
| `friends` | Friendship relationships (pending / accepted / blocked) |
|
||||
| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) |
|
||||
| `messages` | Encrypted payloads (offline buffer, deletable after delivery) |
|
||||
| `moments` | Social posts (text ≤ 1024 chars) |
|
||||
| `moment_images` | Post images (up to 9 per post) |
|
||||
| `moment_likes` | Likes (unique per user per post) |
|
||||
| `moment_comments` | Comments (≤ 512 chars each) |
|
||||
| `push_subscriptions` | Web Push subscriptions (VAPID) |
|
||||
| `onesignal_players` | OneSignal device registrations (Median.co) |
|
||||
| 변수 | 설명 | 기본값 |
|
||||
|------|------|--------|
|
||||
| `PORT` | 서버 포트 | `3000` |
|
||||
| `JWT_SECRET` | JWT 서명 키 (**프로덕션에서 반드시 변경**) | dev_secret |
|
||||
| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL 연결 | — |
|
||||
| `REDIS_HOST` / `REDIS_PASS` | Redis 연결 | — |
|
||||
| `R2_ACCOUNT_ID` | Cloudflare 계정 ID | — |
|
||||
| `R2_ACCESS_KEY_ID` | R2 API 토큰 액세스 키 | — |
|
||||
| `R2_SECRET_ACCESS_KEY` | R2 API 토큰 시크릿 키 | — |
|
||||
| `R2_BUCKET` | R2 버킷 이름 | — |
|
||||
| `R2_PUBLIC_URL` | R2 공개 기본 URL (선택) — CDN 직접 링크 활성화 | — |
|
||||
| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (선택) | — |
|
||||
| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (선택) | — |
|
||||
| `VAPID_PUBLIC_KEY` | Web Push VAPID 공개 키 (선택) | — |
|
||||
| `VAPID_PRIVATE_KEY` | Web Push VAPID 개인 키 (선택) | — |
|
||||
| `VAPID_SUBJECT` | VAPID 연락처 이메일 (선택) | `mailto:admin@paperphone.app` |
|
||||
| `ONESIGNAL_APP_ID` | OneSignal App ID (선택, Median.co용) | — |
|
||||
| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (선택) | — |
|
||||
|
||||
---
|
||||
|
||||
## Security Model
|
||||
|
||||
```
|
||||
On Registration:
|
||||
Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys)
|
||||
Public keys are uploaded; private keys stay on-device (four-tier persistence)
|
||||
|
||||
On Each Message:
|
||||
Sender fetches recipient's IK public key
|
||||
Generates a fresh ephemeral ECDH keypair (per-message)
|
||||
X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt
|
||||
Ephemeral public key sent in message header; destroyed after use
|
||||
|
||||
What the Server Sees:
|
||||
✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps)
|
||||
❌ Plaintext / private keys / ephemeral keys / call content
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PORT` | Server port | `3000` |
|
||||
| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret |
|
||||
| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — |
|
||||
| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — |
|
||||
| `R2_ACCOUNT_ID` | Cloudflare account ID | — |
|
||||
| `R2_ACCESS_KEY_ID` | R2 API token access key | — |
|
||||
| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — |
|
||||
| `R2_BUCKET` | R2 bucket name | — |
|
||||
| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — |
|
||||
| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — |
|
||||
| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — |
|
||||
| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — |
|
||||
| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — |
|
||||
| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` |
|
||||
| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — |
|
||||
| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — |
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
## 라이선스
|
||||
|
||||
MIT © PaperPhone Contributors
|
||||
|
||||
388
README_RU.md
388
README_RU.md
@@ -10,367 +10,173 @@
|
||||
|
||||
---
|
||||
<img width=30% height=30% src="https://raw.githubusercontent.com/619dev/PaperPhone/main/ui.jpg" alt="ui">
|
||||
## Features
|
||||
|
||||
| Feature | Description |
|
||||
|---------|-------------|
|
||||
| 🔐 End-to-End Encryption | Stateless ECDH + XSalsa20-Poly1305 — ephemeral keys per message, forward secrecy |
|
||||
| 🗑️ Zero-Knowledge Server | Server stores only ciphertext; private keys never leave the device |
|
||||
| 📹 Video & Voice Calls | WebRTC P2P (1:1) + Mesh (group), Cloudflare TURN for NAT traversal |
|
||||
| 👥 Group Chat | Up to 2000 members, plain-text messages (no encryption), Do Not Disturb mode, member management |
|
||||
| ⏱️ Auto-Delete Messages | 5 tiers (never / 1 day / 3 days / 1 week / 1 month), settable by either party in DMs, owner-only in groups |
|
||||
| 🔔 Push Notifications | Web Push (VAPID) + OneSignal dual-channel — reach users even when offline |
|
||||
| 🌐 Multi-Language | Chinese, English, Japanese, Korean, French, German, Russian, Spanish — auto-detect + manual switch |
|
||||
| 📱 iOS — No Enterprise Cert | PWA via Safari "Add to Home Screen", works permanently without Apple signing |
|
||||
| 💬 Rich Messaging | Text, images, voice messages, 64-emoji panel, delivery receipts, typing indicators |
|
||||
| 🌐 Moments | WeChat-style social feed: text + up to 9 photos, likes (friend avatars), comments, tag-based visibility control |
|
||||
| 🏷️ Friend Tags | Assign multiple tags to friends (12-color preset palette), filter contacts by tag |
|
||||
| 🗂️ R2 Object Storage | Cloudflare R2 for image/voice files — optional public CDN URL |
|
||||
| 🏗️ Self-Hostable | Docker Compose one-command deployment; Node.js + Redis multi-node ready |
|
||||
## Возможности
|
||||
|
||||
| Функция | Описание |
|
||||
|---------|----------|
|
||||
| 🔐 Сквозное шифрование | Безсостоянийный ECDH + XSalsa20-Poly1305 — эфемерные ключи на каждое сообщение, прямая секретность |
|
||||
| 🗝️ Сервер с нулевым знанием | Сервер хранит только шифротекст; закрытые ключи никогда не покидают устройство |
|
||||
| 📹 Видео/аудио звонки | WebRTC P2P (1:1) + Mesh (группа), Cloudflare TURN для NAT-обхода |
|
||||
| 👥 Групповой чат | До 2000 участников, обычный текст (без шифрования), режим «Не беспокоить», управление участниками |
|
||||
| ⏱️ Автоудаление | 5 уровней (никогда/1д/3д/1нед/1мес), в ЛС — обе стороны, в группах — только владелец |
|
||||
| 🔔 Push-уведомления | Web Push (VAPID) + OneSignal — двойной канал |
|
||||
| 🌐 Многоязычность | ZH/EN/JA/KO/FR/DE/RU/ES — автоопределение + ручной выбор |
|
||||
| 📱 iOS без сертификата | PWA через Safari «На экран Домой», без корпоративного сертификата Apple |
|
||||
| 💬 Расширенные сообщения | Текст, изображения, голосовые сообщения, 64 эмодзи, подтверждения прочтения |
|
||||
| 🌐 Моменты | Социальная лента: текст + до 9 фото, лайки (аватары друзей), комментарии, управление видимостью по тегам |
|
||||
| 🏷️ Теги друзей | Несколько тегов на друга (палитра из 12 цветов), фильтрация контактов по тегам |
|
||||
| 🗂️ Хранилище R2 | Cloudflare R2 для изображений/аудио — опциональный CDN URL |
|
||||
| 🏗️ Самостоятельное размещение | Docker Compose в одну команду |
|
||||
|
||||
---
|
||||
|
||||
## Tech Stack
|
||||
## Технологический стек
|
||||
|
||||
```
|
||||
Backend (server/)
|
||||
Бэкенд (server/)
|
||||
Node.js 20 + Express + ws
|
||||
MySQL 8.0 — users, messages (persisted ciphertext)
|
||||
Redis — online presence + cross-node routing
|
||||
Cloudflare R2 — image/voice file storage (S3-compatible API)
|
||||
JWT + bcrypt authentication
|
||||
MySQL 8.0 — пользователи, сообщения (шифротекст)
|
||||
Redis — статус онлайн + маршрутизация между узлами
|
||||
Cloudflare R2 — хранилище файлов (S3-совместимый API)
|
||||
JWT + bcrypt аутентификация
|
||||
|
||||
Frontend (client/)
|
||||
Native HTML + Vanilla JS (ESM, no bundler required)
|
||||
Фронтенд (client/)
|
||||
Нативный HTML + Vanilla JS (ESM, без сборщика)
|
||||
libsodium-wrappers (WebAssembly — Curve25519 / XSalsa20-Poly1305)
|
||||
WebRTC API — video / voice calls
|
||||
WebRTC API — видео/аудио звонки
|
||||
PWA: manifest.json + Service Worker
|
||||
|
||||
Cryptographic Layer
|
||||
Stateless ECDH + XSalsa20-Poly1305 — ephemeral keypair per message
|
||||
Four-tier key persistence: memory → localStorage → sessionStorage → IndexedDB
|
||||
All private keys stored on-device only — never sent to the server
|
||||
Криптографический слой
|
||||
Безсостоянийный ECDH + XSalsa20-Poly1305 — эфемерная пара ключей на сообщение
|
||||
4-уровневое хранение ключей: память → localStorage → sessionStorage → IndexedDB
|
||||
Закрытые ключи только на устройстве — никогда не отправляются на сервер
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
## Быстрый старт
|
||||
|
||||
### Option 0: Zeabur One-Click Cloud Deploy
|
||||
### Вариант 0: Zeabur — облачное развёртывание в один клик
|
||||
|
||||
[](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
|
||||
|
||||
> [!NOTE]
|
||||
> One manual step is required after the template deploys, otherwise login/register won't work:
|
||||
> 1. Go to Zeabur Console → **server service** → Environment Variables → copy the value of `ZEABUR_WEB_URL` (e.g. `http://10.43.x.x:3000`)
|
||||
> 2. Go to **client service** → Environment Variables → add variable `SERVER_URL` = the value copied above
|
||||
> 3. Restart the client service
|
||||
> После развёртывания шаблона необходим один ручной шаг:
|
||||
> 1. Консоль Zeabur → сервис **server** → Переменные → скопировать `ZEABUR_WEB_URL`
|
||||
> 2. Сервис **client** → Переменные → добавить `SERVER_URL` = скопированное значение
|
||||
> 3. Перезапустить сервис client
|
||||
|
||||
**Known notes:**
|
||||
- On first startup, the server automatically creates all database tables (`CREATE TABLE IF NOT EXISTS`) — no manual SQL import needed
|
||||
- Redis runs without a password inside the cluster (intra-cluster network isolation is sufficient)
|
||||
- If MySQL access is denied, manually set `DB_PASS` on the server service to the value of `MYSQL_ROOT_PASSWORD` from the MySQL service
|
||||
- To get the **internal IP** of any service container, open that service's Terminal in the Zeabur console and run:
|
||||
```bash
|
||||
hostname -i
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Option 1: Docker Compose (Recommended — no local build needed)
|
||||
### Вариант 1: Docker Compose (рекомендуется)
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone <repo-url> && cd paperphone
|
||||
|
||||
# Copy and edit environment variables
|
||||
cp server/.env.example server/.env
|
||||
# Fill in: DB_PASS / JWT_SECRET / CF_CALLS_APP_ID etc.
|
||||
|
||||
# Pull images and start everything
|
||||
# Заполнить: DB_PASS / JWT_SECRET / R2_* и т.д.
|
||||
docker compose up -d
|
||||
|
||||
# Check service status
|
||||
docker compose ps
|
||||
|
||||
# Open in browser
|
||||
open http://localhost
|
||||
```
|
||||
|
||||
> Pre-built images on Docker Hub:
|
||||
> - `facilisvelox/paperphone-client:latest`
|
||||
> - `facilisvelox/paperphone-server:latest`
|
||||
>
|
||||
> **Note**: The server automatically initialises the database schema on first startup — no manual SQL import required.
|
||||
> Образы Docker Hub: `facilisvelox/paperphone-client:latest` и `facilisvelox/paperphone-server:latest`
|
||||
|
||||
### Option 2: Manual Local Start
|
||||
|
||||
#### 1. Prepare the environment
|
||||
### Вариант 2: Локальный ручной запуск
|
||||
|
||||
```bash
|
||||
# Copy and edit environment variables
|
||||
cp server/.env.example server/.env
|
||||
# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
|
||||
# Бэкенд
|
||||
cd server && npm install && npm run dev # → http://localhost:3000
|
||||
|
||||
# Note: the server auto-runs schema.sql on first startup
|
||||
```
|
||||
|
||||
#### 2. Start the backend
|
||||
|
||||
```bash
|
||||
cd server
|
||||
npm install
|
||||
npm run dev # → http://localhost:3000
|
||||
```
|
||||
|
||||
#### 3. Start the frontend
|
||||
|
||||
```bash
|
||||
npx serve client -p 8080
|
||||
# → http://localhost:8080
|
||||
# Фронтенд
|
||||
npx serve client -p 8080 # → http://localhost:8080
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Video Call Configuration
|
||||
|
||||
Video and voice calls use WebRTC P2P and work out of the box on the same LAN. For cross-network calls, a TURN server is required for NAT traversal.
|
||||
|
||||
### Using Cloudflare TURN (Recommended)
|
||||
|
||||
1. Log in to [Cloudflare Dashboard](https://dash.cloudflare.com) → **Workers & Pages** → **Calls** → create an App
|
||||
2. Copy the **App ID** and **App Secret** (Token Key)
|
||||
3. Add them to `server/.env`:
|
||||
## Настройка видеозвонков — Cloudflare TURN
|
||||
|
||||
```env
|
||||
CF_CALLS_APP_ID=your_app_id_here
|
||||
CF_CALLS_APP_SECRET=your_app_secret_here
|
||||
```
|
||||
|
||||
4. Restart the backend — TURN credentials are fetched automatically per call session (TTL: 86 400 s)
|
||||
| Тип | Транспорт | Рекомендуется для |
|
||||
|-----|-----------|-------------------|
|
||||
| 1:1 Видео | WebRTC P2P + TURN | Все сценарии |
|
||||
| 1:1 Голос | WebRTC P2P + TURN | Все сценарии |
|
||||
| Групповой | WebRTC Mesh | До 6 участников |
|
||||
|
||||
> **Without credentials**: the server falls back to STUN only (Google + Cloudflare public STUN). Calls work on LAN without any extra configuration.
|
||||
|
||||
### Call Types
|
||||
|
||||
| Type | Transport | Recommended for |
|
||||
|------|-----------|-----------------|
|
||||
| 1:1 Video Call | WebRTC P2P + TURN | All scenarios |
|
||||
| 1:1 Voice Call | WebRTC P2P + TURN | All scenarios |
|
||||
| Group Video / Voice | WebRTC Mesh (full-mesh) | Up to 6 participants |
|
||||
> **Без настройки**: только STUN. Звонки в ЛВС работают без дополнительных настроек.
|
||||
|
||||
---
|
||||
|
||||
## Push Notification Configuration
|
||||
## Push-уведомления
|
||||
|
||||
Offline message notifications are delivered through **two channels** for maximum delivery rate:
|
||||
|
||||
| Channel | Platforms | Configuration |
|
||||
|---------|-----------|---------------|
|
||||
| Web Push (VAPID) | Browsers (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) | VAPID keys |
|
||||
| OneSignal | Native Android/iOS apps via Median.co | OneSignal App ID + REST Key |
|
||||
|
||||
### Configuring Web Push
|
||||
|
||||
1. Generate VAPID keys (one-time):
|
||||
| Канал | Платформы | Настройка |
|
||||
|-------|-----------|-----------|
|
||||
| Web Push | Браузеры + iOS PWA (Safari 16.4+) | Ключи VAPID |
|
||||
| OneSignal | Нативные приложения Median.co | App ID + REST Key |
|
||||
|
||||
```bash
|
||||
cd server
|
||||
npx web-push generate-vapid-keys
|
||||
```
|
||||
|
||||
2. Add to `server/.env`:
|
||||
---
|
||||
|
||||
```env
|
||||
VAPID_PUBLIC_KEY=your_public_key_here
|
||||
VAPID_PRIVATE_KEY=your_private_key_here
|
||||
VAPID_SUBJECT=mailto:admin@your-domain.com
|
||||
```
|
||||
## iOS — Постоянная установка без сертификата
|
||||
|
||||
3. Restart the server — users can enable notifications from the Settings page
|
||||
|
||||
> **iOS users** must first "Add to Home Screen" (PWA), and only iOS 16.4+ is supported.
|
||||
|
||||
### Configuring OneSignal (Median.co Native Apps)
|
||||
|
||||
1. Create an app on [OneSignal Dashboard](https://onesignal.com) and configure Firebase
|
||||
2. Enable OneSignal in Median.co and enter the App ID
|
||||
3. Add the OneSignal **App ID** and **REST API Key** to `server/.env`:
|
||||
|
||||
```env
|
||||
ONESIGNAL_APP_ID=your_onesignal_app_id
|
||||
ONESIGNAL_REST_KEY=your_onesignal_rest_api_key
|
||||
```
|
||||
|
||||
> **When not configured**: push notifications are silently disabled — all other features work normally.
|
||||
1. Развернуть на HTTPS-сервере → 2. Открыть в Safari → 3. Поделиться ⬆️ → 4. «На экран Домой»
|
||||
|
||||
---
|
||||
|
||||
## iOS — Permanent No-Cert Deployment
|
||||
## Модель безопасности
|
||||
|
||||
1. Deploy to a server with an HTTPS domain (required for WebRTC and Web Crypto APIs)
|
||||
2. Open `https://your.domain.com` in **Safari**
|
||||
3. Tap the Share button ⬆️ at the bottom of the screen
|
||||
4. Select **Add to Home Screen** → **Add**
|
||||
|
||||
The app behaves identically to a native app — no Apple enterprise certificate required, no expiry.
|
||||
|
||||
---
|
||||
|
||||
## Production Deployment (Nginx)
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name your.domain.com;
|
||||
|
||||
ssl_certificate /path/to/cert.pem;
|
||||
ssl_certificate_key /path/to/key.pem;
|
||||
|
||||
# Frontend static files
|
||||
location / {
|
||||
root /path/to/paperphone/client;
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
|
||||
# REST API
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# WebSocket (messaging + call signalling)
|
||||
location /ws {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_read_timeout 3600s;
|
||||
}
|
||||
}
|
||||
```
|
||||
Регистрация: IK + SPK + 10×OPK генерируются локально, публичные ключи загружаются
|
||||
Сообщение: Эфемерный ECDH → X25519 → XSalsa20-Poly1305
|
||||
Сервер видит: ✅ шифротекст + метаданные маршрутизации ❌ открытый текст / закрытые ключи
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Project Structure
|
||||
## Схема базы данных
|
||||
|
||||
```
|
||||
paperphone/
|
||||
├── docker-compose.yml
|
||||
├── server/
|
||||
│ ├── .env # Environment variables (incl. Cloudflare TURN keys)
|
||||
│ └── src/
|
||||
│ ├── app.js # Express application
|
||||
│ ├── routes/
|
||||
│ │ ├── auth.js # Register / Login (incl. X3DH public key upload)
|
||||
│ │ ├── users.js # User search / Prekey bundle download
|
||||
│ │ ├── friends.js # Friend requests / Accept (incl. offline push)
|
||||
│ │ ├── groups.js # Group management
|
||||
│ │ ├── messages.js # Historical messages (paginated ciphertext)
|
||||
│ │ ├── upload.js # Cloudflare R2 file upload
|
||||
│ │ ├── files.js # File proxy (when R2_PUBLIC_URL is not set)
|
||||
│ │ ├── moments.js # Moments feed (posts / likes / comments)
|
||||
│ │ ├── calls.js # TURN credential issuance
|
||||
│ │ └── push.js # Push subscription mgmt (Web Push + OneSignal)
|
||||
│ ├── services/
|
||||
│ │ ├── push.js # Web Push VAPID service
|
||||
│ │ └── onesignal.js # OneSignal REST API service
|
||||
│ └── ws/
|
||||
│ └── wsServer.js # WebSocket router (messages + call signalling + offline push)
|
||||
│
|
||||
└── client/
|
||||
├── index.html # SPA entry + PWA meta + Median push bridge
|
||||
├── manifest.json # PWA manifest
|
||||
├── sw.js # Service Worker (offline cache + push notifications)
|
||||
└── src/
|
||||
├── style.css # Premium design system (dark/light, glassmorphism)
|
||||
├── app.js # Router + global state + incoming call listener
|
||||
├── api.js # HTTP client
|
||||
├── socket.js # WebSocket client (auto-reconnect)
|
||||
├── i18n.js # Multi-language engine (zh / en / ja / ko / fr / de / ru / es)
|
||||
├── services/
|
||||
│ ├── webrtc.js # WebRTC manager — CallManager class
|
||||
│ └── pushNotification.js # Push subscription mgmt (Web Push + Median bridge)
|
||||
├── crypto/
|
||||
│ ├── ratchet.js # X3DH + Double Ratchet + ML-KEM-768
|
||||
│ └── keystore.js # IndexedDB private key store
|
||||
├── pages/
|
||||
│ ├── login.js # Login / Register (key generation, language picker)
|
||||
│ ├── chats.js # Chat list
|
||||
│ ├── chat.js # Chat window (E2E encryption, call buttons)
|
||||
│ ├── groups.js # Group list (create group, search)
|
||||
│ ├── groupInfo.js # Group info (member management, DND, leave/disband)
|
||||
│ ├── contacts.js # Contacts (friend requests, online status)
|
||||
│ ├── discover.js # Discover page
|
||||
│ ├── profile.js # Me / Settings (language, fingerprint, notifications, PWA)
|
||||
│ └── call.js # Call UI (incoming / active / multi-party video)
|
||||
```
|
||||
11 таблиц, создаются автоматически при первом запуске:
|
||||
|
||||
| Таблица | Назначение |
|
||||
|---------|------------|
|
||||
| `users` | Профили + публичные ключи ECDH/OPK |
|
||||
| `prekeys` | Одноразовые предключи X3DH |
|
||||
| `friends` | Связи дружбы |
|
||||
| `groups` / `group_members` | Группы + участники |
|
||||
| `messages` | Зашифрованные сообщения |
|
||||
| `moments` / `moment_images` | Посты + изображения |
|
||||
| `moment_likes` / `moment_comments` | Лайки + комментарии |
|
||||
| `push_subscriptions` | Web Push (VAPID) |
|
||||
| `onesignal_players` | OneSignal устройства |
|
||||
|
||||
---
|
||||
|
||||
## Database Schema
|
||||
## Переменные окружения
|
||||
|
||||
11 tables, auto-created on first server startup (`CREATE TABLE IF NOT EXISTS`):
|
||||
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `users` | User profiles + ECDH/OPK public keys |
|
||||
| `prekeys` | X3DH one-time prekey pool |
|
||||
| `friends` | Friendship relationships (pending / accepted / blocked) |
|
||||
| `groups` / `group_members` | Group chats + membership (incl. mute/DND status) |
|
||||
| `messages` | Encrypted payloads (offline buffer, deletable after delivery) |
|
||||
| `moments` | Social posts (text ≤ 1024 chars) |
|
||||
| `moment_images` | Post images (up to 9 per post) |
|
||||
| `moment_likes` | Likes (unique per user per post) |
|
||||
| `moment_comments` | Comments (≤ 512 chars each) |
|
||||
| `push_subscriptions` | Web Push subscriptions (VAPID) |
|
||||
| `onesignal_players` | OneSignal device registrations (Median.co) |
|
||||
| Переменная | Описание | По умолчанию |
|
||||
|------------|----------|-------------|
|
||||
| `PORT` | Порт сервера | `3000` |
|
||||
| `JWT_SECRET` | Ключ подписи JWT (**изменить в продакшене**) | dev_secret |
|
||||
| `DB_HOST`/`DB_PASS`/`DB_NAME` | Подключение MySQL | — |
|
||||
| `REDIS_HOST`/`REDIS_PASS` | Подключение Redis | — |
|
||||
| `R2_ACCOUNT_ID` | ID аккаунта Cloudflare | — |
|
||||
| `R2_ACCESS_KEY_ID` | Ключ доступа R2 API | — |
|
||||
| `R2_SECRET_ACCESS_KEY` | Секретный ключ R2 API | — |
|
||||
| `R2_BUCKET` | Имя бакета R2 | — |
|
||||
| `R2_PUBLIC_URL` | Публичный URL R2 (опц.) | — |
|
||||
| `CF_CALLS_APP_ID` | Calls App ID (опц.) | — |
|
||||
| `CF_CALLS_APP_SECRET` | Calls Secret (опц.) | — |
|
||||
| `VAPID_PUBLIC_KEY` | Публичный ключ VAPID (опц.) | — |
|
||||
| `VAPID_PRIVATE_KEY` | Закрытый ключ VAPID (опц.) | — |
|
||||
| `ONESIGNAL_APP_ID` | OneSignal App ID (опц.) | — |
|
||||
| `ONESIGNAL_REST_KEY` | OneSignal REST Key (опц.) | — |
|
||||
|
||||
---
|
||||
|
||||
## Security Model
|
||||
|
||||
```
|
||||
On Registration:
|
||||
Device generates IK (Identity Key) + SPK (Signed PreKey) + 10× OPK (One-Time PreKeys)
|
||||
Public keys are uploaded; private keys stay on-device (four-tier persistence)
|
||||
|
||||
On Each Message:
|
||||
Sender fetches recipient's IK public key
|
||||
Generates a fresh ephemeral ECDH keypair (per-message)
|
||||
X25519 ECDH → shared secret → XSalsa20-Poly1305 encrypt
|
||||
Ephemeral public key sent in message header; destroyed after use
|
||||
|
||||
What the Server Sees:
|
||||
✅ Ciphertext blob + routing metadata (sender / recipient UUID, timestamps)
|
||||
❌ Plaintext / private keys / ephemeral keys / call content
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `PORT` | Server port | `3000` |
|
||||
| `JWT_SECRET` | JWT signing key (**change in production**) | dev_secret |
|
||||
| `DB_HOST` / `DB_PASS` / `DB_NAME` | MySQL connection | — |
|
||||
| `REDIS_HOST` / `REDIS_PASS` | Redis connection | — |
|
||||
| `R2_ACCOUNT_ID` | Cloudflare account ID | — |
|
||||
| `R2_ACCESS_KEY_ID` | R2 API token access key | — |
|
||||
| `R2_SECRET_ACCESS_KEY` | R2 API token secret key | — |
|
||||
| `R2_BUCKET` | R2 bucket name | — |
|
||||
| `R2_PUBLIC_URL` | R2 public base URL (optional) — enables direct CDN links | — |
|
||||
| `CF_CALLS_APP_ID` | Cloudflare Calls App ID (optional) | — |
|
||||
| `CF_CALLS_APP_SECRET` | Cloudflare Calls App Secret (optional) | — |
|
||||
| `VAPID_PUBLIC_KEY` | Web Push VAPID public key (optional) | — |
|
||||
| `VAPID_PRIVATE_KEY` | Web Push VAPID private key (optional) | — |
|
||||
| `VAPID_SUBJECT` | VAPID contact email (optional) | `mailto:admin@paperphone.app` |
|
||||
| `ONESIGNAL_APP_ID` | OneSignal App ID (optional, for Median.co) | — |
|
||||
| `ONESIGNAL_REST_KEY` | OneSignal REST API Key (optional) | — |
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
## Лицензия
|
||||
|
||||
MIT © PaperPhone Contributors
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
# Nginx config for PaperPhone — serves client and proxies API to server
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# Nginx config for PaperPhone — serves PWA + reverse-proxy to API
|
||||
#
|
||||
# This file is an envsubst template. At container start the
|
||||
# Dockerfile CMD substitutes ${SERVER_URL} and writes the final
|
||||
# config to /etc/nginx/conf.d/paperphone.conf.
|
||||
#
|
||||
# Docker Compose: SERVER_URL = http://server:3000
|
||||
# Zeabur: SERVER_URL = ${ZEABUR_WEB_URL} (e.g. http://10.43.x.x:3000)
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
@@ -7,39 +16,42 @@ server {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Gzip compression
|
||||
# ── Compression ──────────────────────────────────────────────
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/javascript application/json application/wasm;
|
||||
gzip_min_length 1024;
|
||||
gzip_vary on;
|
||||
|
||||
# Security headers
|
||||
# ── Security headers ─────────────────────────────────────────
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-Frame-Options "DENY" always;
|
||||
add_header Referrer-Policy "no-referrer" always;
|
||||
|
||||
# Cache PWA assets aggressively (content-hashed in prod)
|
||||
# ── Static asset caching ─────────────────────────────────────
|
||||
location ~* \.(css|js|wasm|png|jpg|webp|ico|svg|woff2?)$ {
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, max-age=604800, immutable";
|
||||
}
|
||||
|
||||
# Service worker — no-cache so updates propagate immediately
|
||||
# Service worker — always revalidate so updates propagate
|
||||
location = /sw.js {
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
expires 0;
|
||||
}
|
||||
|
||||
# WebSocket upgrade — proxy to backend
|
||||
# ── WebSocket — proxy to backend ─────────────────────────────
|
||||
location /ws {
|
||||
proxy_pass ${SERVER_URL};
|
||||
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 3600s;
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
# REST API — proxy to backend
|
||||
# ── REST API — proxy to backend ──────────────────────────────
|
||||
location /api/ {
|
||||
proxy_pass ${SERVER_URL};
|
||||
proxy_http_version 1.1;
|
||||
@@ -48,14 +60,18 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
client_max_body_size 51m;
|
||||
|
||||
# Long-poll & upload timeout
|
||||
proxy_read_timeout 120s;
|
||||
proxy_send_timeout 120s;
|
||||
}
|
||||
|
||||
# Health check from backend
|
||||
# ── Health check (forwarded to backend) ──────────────────────
|
||||
location /health {
|
||||
proxy_pass ${SERVER_URL};
|
||||
}
|
||||
|
||||
# SPA fallback — all other routes serve index.html
|
||||
# ── SPA fallback ─────────────────────────────────────────────
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
version: "3.9"
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# PaperPhone — Full-Stack Docker Compose
|
||||
#
|
||||
@@ -8,14 +6,17 @@ version: "3.9"
|
||||
# server — Node.js API + WebSocket
|
||||
# mysql — Message store / user DB
|
||||
# redis — Online presence & session cache
|
||||
# minio — S3-compatible object storage for media uploads
|
||||
#
|
||||
# File storage:
|
||||
# Cloudflare R2 (configured via R2_* env vars in server/.env)
|
||||
# No MinIO / S3 container needed.
|
||||
#
|
||||
# Images:
|
||||
# docker pull facilisvelox/paperphone-client:latest
|
||||
# docker pull facilisvelox/paperphone-server:latest
|
||||
#
|
||||
# Quick start:
|
||||
# cp server/.env.example server/.env # edit secrets & CF_CALLS_APP_ID
|
||||
# cp server/.env.example server/.env # edit secrets & R2 keys
|
||||
# docker compose up -d
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -26,6 +27,8 @@ services:
|
||||
image: facilisvelox/paperphone-client:latest
|
||||
ports:
|
||||
- "80:80"
|
||||
environment:
|
||||
SERVER_URL: http://server:3000
|
||||
depends_on:
|
||||
server:
|
||||
condition: service_healthy
|
||||
@@ -40,17 +43,16 @@ services:
|
||||
environment:
|
||||
DB_HOST: mysql
|
||||
DB_PORT: 3306
|
||||
DB_USER: ${DB_USER:-paperphone}
|
||||
DB_PASS: ${DB_PASS:-changeme}
|
||||
DB_NAME: paperphone
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: 6379
|
||||
MINIO_ENDPOINT: minio
|
||||
MINIO_PORT: 9000
|
||||
depends_on:
|
||||
mysql:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_started
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
|
||||
interval: 30s
|
||||
@@ -97,30 +99,9 @@ services:
|
||||
networks:
|
||||
- paperphone
|
||||
|
||||
# ── MinIO (S3-compatible) ─────────────────────────────────────
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY:-minioadmin}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY:-minioadmin}
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
ports:
|
||||
- "9001:9001" # MinIO console (remove in prod if not needed)
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- paperphone
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
||||
redis_data:
|
||||
minio_data:
|
||||
|
||||
networks:
|
||||
paperphone:
|
||||
|
||||
13
server/.env
13
server/.env
@@ -10,12 +10,13 @@ DB_NAME=paperphone
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASS=
|
||||
MINIO_ENDPOINT=localhost
|
||||
MINIO_PORT=9000
|
||||
MINIO_USE_SSL=false
|
||||
MINIO_ACCESS_KEY=minioadmin
|
||||
MINIO_SECRET_KEY=minioadmin
|
||||
MINIO_BUCKET=paperphone
|
||||
|
||||
# Cloudflare R2 (S3-compatible object storage)
|
||||
R2_ACCOUNT_ID=
|
||||
R2_ACCESS_KEY_ID=
|
||||
R2_SECRET_ACCESS_KEY=
|
||||
R2_BUCKET=paperphone
|
||||
R2_PUBLIC_URL=
|
||||
|
||||
# Cloudflare Calls (TURN service)
|
||||
# Get from: Cloudflare Dashboard → Calls → Your App → API Keys
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
# PaperPhone — Production Environment Variables
|
||||
# PaperPhone — Production Environment Variables (Docker Compose)
|
||||
# Copy this file to server/.env and fill in real values
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -13,7 +13,7 @@ JWT_SECRET=CHANGE_ME_USE_OPENSSL_RAND_HEX_32
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
# ── MySQL ─────────────────────────────────────────────────────────
|
||||
# In Docker, DB_HOST is the service name: mysql
|
||||
# In Docker Compose, DB_HOST is the service name: mysql
|
||||
DB_HOST=mysql
|
||||
DB_PORT=3306
|
||||
DB_USER=paperphone
|
||||
@@ -21,17 +21,30 @@ DB_PASS=CHANGE_ME_DB_PASSWORD
|
||||
DB_NAME=paperphone
|
||||
|
||||
# ── Redis ─────────────────────────────────────────────────────────
|
||||
# In Docker, REDIS_HOST is the service name: redis
|
||||
# In Docker Compose, REDIS_HOST is the service name: redis
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
# Optional password (matches redis command --requirepass)
|
||||
REDIS_PASS=
|
||||
|
||||
# ── MinIO ─────────────────────────────────────────────────────────
|
||||
# In Docker, MINIO_ENDPOINT is the service name: minio
|
||||
MINIO_ENDPOINT=minio
|
||||
MINIO_PORT=9000
|
||||
MINIO_USE_SSL=false
|
||||
MINIO_ACCESS_KEY=CHANGE_ME_MINIO_ACCESS
|
||||
MINIO_SECRET_KEY=CHANGE_ME_MINIO_SECRET
|
||||
MINIO_BUCKET=paperphone
|
||||
# ── Cloudflare R2 (S3-compatible object storage) ─────────────────
|
||||
# Get from: Cloudflare Dashboard → R2 → Manage API Tokens
|
||||
R2_ACCOUNT_ID=
|
||||
R2_ACCESS_KEY_ID=
|
||||
R2_SECRET_ACCESS_KEY=
|
||||
R2_BUCKET=paperphone
|
||||
R2_PUBLIC_URL=
|
||||
|
||||
# ── Cloudflare TURN (optional — for cross-network WebRTC calls) ──
|
||||
# Get from: Cloudflare Dashboard → Workers & Pages → Calls
|
||||
CF_CALLS_APP_ID=
|
||||
CF_CALLS_APP_SECRET=
|
||||
|
||||
# ── Web Push / VAPID (optional) ──────────────────────────────────
|
||||
# Generate with: npx web-push generate-vapid-keys
|
||||
VAPID_PUBLIC_KEY=
|
||||
VAPID_PRIVATE_KEY=
|
||||
VAPID_SUBJECT=mailto:admin@paperphone.app
|
||||
|
||||
# ── OneSignal (optional — Median.co native push) ─────────────────
|
||||
ONESIGNAL_APP_ID=
|
||||
ONESIGNAL_REST_KEY=
|
||||
|
||||
@@ -18,21 +18,26 @@ REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASS=
|
||||
|
||||
# MinIO
|
||||
MINIO_ENDPOINT=localhost
|
||||
MINIO_PORT=9000
|
||||
MINIO_USE_SSL=false
|
||||
MINIO_ACCESS_KEY=minioadmin
|
||||
MINIO_SECRET_KEY=minioadmin
|
||||
MINIO_BUCKET=paperphone
|
||||
# Cloudflare R2 (S3-compatible object storage)
|
||||
# Get from Cloudflare Dashboard → R2 → Manage API Tokens
|
||||
R2_ACCOUNT_ID=
|
||||
R2_ACCESS_KEY_ID=
|
||||
R2_SECRET_ACCESS_KEY=
|
||||
R2_BUCKET=paperphone
|
||||
R2_PUBLIC_URL=
|
||||
|
||||
# Web Push (VAPID)
|
||||
# Cloudflare TURN (optional — for cross-network WebRTC calls)
|
||||
# Get from Cloudflare Dashboard → Workers & Pages → Calls
|
||||
CF_CALLS_APP_ID=
|
||||
CF_CALLS_APP_SECRET=
|
||||
|
||||
# Web Push (VAPID) — optional
|
||||
# Generate with: npx web-push generate-vapid-keys
|
||||
VAPID_PUBLIC_KEY=
|
||||
VAPID_PRIVATE_KEY=
|
||||
VAPID_SUBJECT=mailto:admin@paperphone.app
|
||||
|
||||
# OneSignal (Median.co native push)
|
||||
# OneSignal (Median.co native push) — optional
|
||||
# Get from OneSignal Dashboard → Settings → Keys & IDs
|
||||
ONESIGNAL_APP_ID=
|
||||
ONESIGNAL_REST_KEY=
|
||||
|
||||
@@ -25,10 +25,11 @@ spec:
|
||||
|
||||
## Features
|
||||
- 📹 **Video & Voice Calls** — WebRTC P2P (1:1) + Mesh (group up to 6)
|
||||
- 🌐 **Multi-language** — Chinese, English, Japanese, Korean, French
|
||||
- 🌐 **Multi-language** — Chinese, English, Japanese, Korean, French, German, Russian, Spanish
|
||||
- 📱 **iOS PWA** — "Add to Home Screen" via Safari, no enterprise cert needed
|
||||
- 💬 Rich messaging: text, images, voice, emoji, delivery receipts, typing indicators
|
||||
- 🗂️ **Cloudflare R2** file storage (images & voice messages)
|
||||
- 🏷️ **Friend Tags** — tag-based contact filtering & Moments visibility control
|
||||
|
||||
## After Deployment
|
||||
1. Open the domain assigned to the **client** service.
|
||||
@@ -386,10 +387,11 @@ localization:
|
||||
|
||||
## 功能亮点
|
||||
- 📹 **视频/语音通话** — WebRTC P2P(1:1)+ Mesh 多人(≤6 人)
|
||||
- 🌐 **多语言** — 中文、英文、日语、韩语、法语
|
||||
- 🌐 **多语言** — 中文、英文、日语、韩语、法语、德语、俄语、西班牙语
|
||||
- 📱 **iOS 永久免签** — Safari「添加到主屏幕」,无需企业证书
|
||||
- 💬 富文本消息、图片、语音、Emoji、送达回执、打字状态
|
||||
- 🗂️ **Cloudflare R2** 对象存储(图片与语音消息)
|
||||
- 🏷️ **好友标签** — 标签分类筛选通讯录 + 朋友圈可见性控制
|
||||
|
||||
## 部署后操作
|
||||
1. 打开分配给 **client** 服务的域名。
|
||||
|
||||
Reference in New Issue
Block a user