增加多国语言说明文档

This commit is contained in:
619dev
2026-03-28 22:08:29 +08:00
parent ec1289d113
commit 10b9e531fc
10 changed files with 2916 additions and 9 deletions

View File

@@ -1,5 +1,7 @@
# PaperPhone IM
🌐 **其他语言 / Other Languages:** [English](README_EN.md) · [日本語](README_JA.md) · [한국어](README_KO.md) · [Français](README_FR.md) · [Deutsch](README_DE.md) · [Русский](README_RU.md) · [Español](README_ES.md)
一款微信风格的端对端加密即时通讯应用,采用无状态 ECDH + XSalsa20-Poly1305 逐消息加密,支持 iOS PWA 永久免签与 Cloudflare R2 文件存储。
[![Node.js](https://img.shields.io/badge/Node.js-20+-green)](#) [![MySQL](https://img.shields.io/badge/MySQL-8.0-blue)](#) [![Redis](https://img.shields.io/badge/Redis-7.x-red)](#) [![WebRTC](https://img.shields.io/badge/WebRTC-P2P%20%2B%20Mesh-orange)](#)
@@ -19,10 +21,11 @@
| 👥 群聊 | 最多 2000 人群组,纯文本消息(无加密),免打扰模式,成员管理 |
| ⏱️ 消息自动删除 | 5 档可选(永不/1天/3天/1周/1月私聊双方均可设置群聊群主专属 |
| 🔔 消息推送 | Web Push (VAPID) + OneSignal 双通道,离线也能收到通知 |
| 🌐 多语言 | 中文、英文、日语、韩语、法语(自动检测 + 手动切换) |
| 🌐 多语言 | 中文、英文、日语、韩语、法语、德语、俄语、西班牙语(自动检测 + 手动切换) |
| 📱 iOS 永久免签 | PWA H5 → Safari「添加到主屏幕」无需企业证书 |
| 💬 消息功能 | 文字、图片、语音消息、Emoji 面板64 个)、已读状态 |
| 🌐 朋友圈 | 发动态(文字+最多9张图、点赞、评论、好友动态流 |
| 🌐 朋友圈 | 发动态(文字+最多9张图、点赞(显示好友头像)、评论、标签可见性控制 |
| 🏷️ 好友标签 | 为好友设置多个标签12色预设调色板按标签分类筛选通讯录 |
| 🗂️ R2 对象存储 | Cloudflare R2 存储图片/语音,可选公开 CDN 直链 |
| 🏗️ 可自托管 | Docker Compose 一键部署,支持 Node.js + Redis 多节点 |
@@ -285,7 +288,7 @@ paperphone/
├── app.js # 路由 + 全局状态 + 来电监听
├── api.js # HTTP 客户端
├── socket.js # WebSocket 客户端(自动重连)
├── i18n.js # 多语言引擎zh/en/ja/ko/fr
├── i18n.js # 多语言引擎zh/en/ja/ko/fr/de/ru/es
├── services/
│ ├── webrtc.js # WebRTC 管理器CallManager
│ └── pushNotification.js # 推送订阅管理Web Push + Median 桥接)

376
README_DE.md Normal file
View File

@@ -0,0 +1,376 @@
# PaperPhone IM
🌐 **Andere Sprachen / Other Languages:** [中文](README.md) · [English](README_EN.md) · [日本語](README_JA.md) · [한국어](README_KO.md) · [Français](README_FR.md) · [Русский](README_RU.md) · [Español](README_ES.md)
Eine Instant-Messaging-App im WeChat-Stil mit Ende-zu-Ende-Verschlüsselung über zustandsloses ECDH + XSalsa20-Poly1305 pro Nachricht, Echtzeit-Videoanrufe, Cloudflare R2 Dateispeicher, Mehrsprachigkeit und iOS-PWA-Bereitstellung.
[![Node.js](https://img.shields.io/badge/Node.js-20+-green)](#) [![MySQL](https://img.shields.io/badge/MySQL-8.0-blue)](#) [![Redis](https://img.shields.io/badge/Redis-7.x-red)](#) [![WebRTC](https://img.shields.io/badge/WebRTC-P2P%20%2B%20Mesh-orange)](#)
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
---
<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 |
---
## Tech 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
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
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
```
---
## Quick Start
### Option 0: Zeabur One-Click Cloud Deploy
[![Deploy on Zeabur](https://zeabur.com/button.svg)](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
**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)
```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
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
```bash
# Copy and edit environment variables
cp server/.env.example server/.env
# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
# 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
```
---
## 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`:
```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 |
---
## 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 |
### Configuring Web Push
1. Generate VAPID keys (one-time):
```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
```
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.
---
## 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;
}
}
```
---
## 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)
```
---
## 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
MIT © PaperPhone Contributors

View File

@@ -1,5 +1,7 @@
# PaperPhone IM
🌐 **Other Languages:** [中文](README.md) · [日本語](README_JA.md) · [한국어](README_KO.md) · [Français](README_FR.md) · [Deutsch](README_DE.md) · [Русский](README_RU.md) · [Español](README_ES.md)
A WeChat-style end-to-end encrypted instant messaging app with stateless ECDH + XSalsa20-Poly1305 per-message encryption, real-time video calls, Cloudflare R2 file storage, multi-language support and iOS PWA deployment.
[![Node.js](https://img.shields.io/badge/Node.js-20+-green)](#) [![MySQL](https://img.shields.io/badge/MySQL-8.0-blue)](#) [![Redis](https://img.shields.io/badge/Redis-7.x-red)](#) [![WebRTC](https://img.shields.io/badge/WebRTC-P2P%20%2B%20Mesh-orange)](#)
@@ -18,10 +20,11 @@ A WeChat-style end-to-end encrypted instant messaging app with stateless ECDH +
| 👥 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 — auto-detect + manual switch |
| 🌐 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, comments |
| 🌐 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 |
@@ -284,7 +287,7 @@ paperphone/
├── 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)
├── 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)

376
README_ES.md Normal file
View File

@@ -0,0 +1,376 @@
# PaperPhone IM
🌐 **Otros idiomas / Other Languages:** [中文](README.md) · [English](README_EN.md) · [日本語](README_JA.md) · [한국어](README_KO.md) · [Français](README_FR.md) · [Deutsch](README_DE.md) · [Русский](README_RU.md)
Una aplicación de mensajería instantánea cifrada de extremo a extremo estilo WeChat, con cifrado ECDH sin estado + XSalsa20-Poly1305 por mensaje, videollamadas en tiempo real, almacenamiento Cloudflare R2, soporte multilingüe y despliegue iOS PWA.
[![Node.js](https://img.shields.io/badge/Node.js-20+-green)](#) [![MySQL](https://img.shields.io/badge/MySQL-8.0-blue)](#) [![Redis](https://img.shields.io/badge/Redis-7.x-red)](#) [![WebRTC](https://img.shields.io/badge/WebRTC-P2P%20%2B%20Mesh-orange)](#)
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
---
<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 |
---
## Tech 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
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
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
```
---
## Quick Start
### Option 0: Zeabur One-Click Cloud Deploy
[![Deploy on Zeabur](https://zeabur.com/button.svg)](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
**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)
```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
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
```bash
# Copy and edit environment variables
cp server/.env.example server/.env
# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
# 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
```
---
## 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`:
```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 |
---
## 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 |
### Configuring Web Push
1. Generate VAPID keys (one-time):
```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
```
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.
---
## 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;
}
}
```
---
## 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)
```
---
## 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
MIT © PaperPhone Contributors

376
README_FR.md Normal file
View File

@@ -0,0 +1,376 @@
# PaperPhone IM
🌐 **Autres langues / Other Languages:** [中文](README.md) · [English](README_EN.md) · [日本語](README_JA.md) · [한국어](README_KO.md) · [Deutsch](README_DE.md) · [Русский](README_RU.md) · [Español](README_ES.md)
Une application de messagerie instantanée chiffrée de bout en bout, style WeChat, avec chiffrement ECDH sans état + XSalsa20-Poly1305 par message, appels vidéo en temps réel, stockage Cloudflare R2, support multilingue et déploiement iOS PWA.
[![Node.js](https://img.shields.io/badge/Node.js-20+-green)](#) [![MySQL](https://img.shields.io/badge/MySQL-8.0-blue)](#) [![Redis](https://img.shields.io/badge/Redis-7.x-red)](#) [![WebRTC](https://img.shields.io/badge/WebRTC-P2P%20%2B%20Mesh-orange)](#)
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
---
<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 |
---
## Tech 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
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
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
```
---
## Quick Start
### Option 0: Zeabur One-Click Cloud Deploy
[![Deploy on Zeabur](https://zeabur.com/button.svg)](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
**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)
```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
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
```bash
# Copy and edit environment variables
cp server/.env.example server/.env
# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
# 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
```
---
## 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`:
```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 |
---
## 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 |
### Configuring Web Push
1. Generate VAPID keys (one-time):
```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
```
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.
---
## 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;
}
}
```
---
## 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)
```
---
## 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
MIT © PaperPhone Contributors

376
README_JA.md Normal file
View File

@@ -0,0 +1,376 @@
# PaperPhone IM
🌐 **他の言語 / Other Languages:** [中文](README.md) · [English](README_EN.md) · [한국어](README_KO.md) · [Français](README_FR.md) · [Deutsch](README_DE.md) · [Русский](README_RU.md) · [Español](README_ES.md)
WeChatスタイルのエンドツーエンド暗号化インスタントメッセージアプリ。ステートレス ECDH + XSalsa20-Poly1305 のメッセージ単位暗号化、リアルタイムビデオ通話、Cloudflare R2 ファイルストレージ、多言語対応、iOS PWA デプロイをサポート。
[![Node.js](https://img.shields.io/badge/Node.js-20+-green)](#) [![MySQL](https://img.shields.io/badge/MySQL-8.0-blue)](#) [![Redis](https://img.shields.io/badge/Redis-7.x-red)](#) [![WebRTC](https://img.shields.io/badge/WebRTC-P2P%20%2B%20Mesh-orange)](#)
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
---
<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 |
---
## Tech 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
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
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
```
---
## Quick Start
### Option 0: Zeabur One-Click Cloud Deploy
[![Deploy on Zeabur](https://zeabur.com/button.svg)](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
**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)
```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
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
```bash
# Copy and edit environment variables
cp server/.env.example server/.env
# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
# 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
```
---
## 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`:
```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 |
---
## 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 |
### Configuring Web Push
1. Generate VAPID keys (one-time):
```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
```
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.
---
## 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;
}
}
```
---
## 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)
```
---
## 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
MIT © PaperPhone Contributors

376
README_KO.md Normal file
View File

@@ -0,0 +1,376 @@
# PaperPhone IM
🌐 **다른 언어 / Other Languages:** [中文](README.md) · [English](README_EN.md) · [日本語](README_JA.md) · [Français](README_FR.md) · [Deutsch](README_DE.md) · [Русский](README_RU.md) · [Español](README_ES.md)
WeChat 스타일의 종단간 암호화 인스턴트 메시징 앱. 무상태 ECDH + XSalsa20-Poly1305 메시지별 암호화, 실시간 영상 통화, Cloudflare R2 파일 저장소, 다국어 지원 및 iOS PWA 배포를 지원합니다.
[![Node.js](https://img.shields.io/badge/Node.js-20+-green)](#) [![MySQL](https://img.shields.io/badge/MySQL-8.0-blue)](#) [![Redis](https://img.shields.io/badge/Redis-7.x-red)](#) [![WebRTC](https://img.shields.io/badge/WebRTC-P2P%20%2B%20Mesh-orange)](#)
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
---
<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 |
---
## Tech 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
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
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
```
---
## Quick Start
### Option 0: Zeabur One-Click Cloud Deploy
[![Deploy on Zeabur](https://zeabur.com/button.svg)](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
**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)
```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
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
```bash
# Copy and edit environment variables
cp server/.env.example server/.env
# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
# 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
```
---
## 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`:
```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 |
---
## 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 |
### Configuring Web Push
1. Generate VAPID keys (one-time):
```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
```
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.
---
## 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;
}
}
```
---
## 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)
```
---
## 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
MIT © PaperPhone Contributors

376
README_RU.md Normal file
View File

@@ -0,0 +1,376 @@
# PaperPhone IM
🌐 **Другие языки / Other Languages:** [中文](README.md) · [English](README_EN.md) · [日本語](README_JA.md) · [한국어](README_KO.md) · [Français](README_FR.md) · [Deutsch](README_DE.md) · [Español](README_ES.md)
Мессенджер в стиле WeChat со сквозным шифрованием — безсостоянийный ECDH + XSalsa20-Poly1305 для каждого сообщения, видеозвонки в реальном времени, хранилище файлов Cloudflare R2, многоязычная поддержка и развёртывание iOS PWA.
[![Node.js](https://img.shields.io/badge/Node.js-20+-green)](#) [![MySQL](https://img.shields.io/badge/MySQL-8.0-blue)](#) [![Redis](https://img.shields.io/badge/Redis-7.x-red)](#) [![WebRTC](https://img.shields.io/badge/WebRTC-P2P%20%2B%20Mesh-orange)](#)
[![Deploy on Zeabur](https://zeabur.com/button.svg)](https://zeabur.com/templates/P2J7Y3?referralCode=619dev)
---
<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 |
---
## Tech 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
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
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
```
---
## Quick Start
### Option 0: Zeabur One-Click Cloud Deploy
[![Deploy on Zeabur](https://zeabur.com/button.svg)](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
**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)
```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
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
```bash
# Copy and edit environment variables
cp server/.env.example server/.env
# Fill in DB_HOST / DB_PASS / REDIS_HOST / R2_* etc.
# 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
```
---
## 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`:
```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 |
---
## 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 |
### Configuring Web Push
1. Generate VAPID keys (one-time):
```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
```
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.
---
## 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;
}
}
```
---
## 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)
```
---
## 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
MIT © PaperPhone Contributors

View File

@@ -1086,13 +1086,655 @@ const TRANSLATIONS = {
likedUsers: 'Aimé par',
nLikes: 'j\'aime',
},
de: {
// App
appName: 'PaperPhone',
appTagline: 'Ende-zu-Ende-Verschlüssellung · Forward Secrecy · Post-Quantum',
keyNotice: 'Schlüssel werden nur auf diesem Gerät gespeichert',
// Auth
username: 'Benutzername',
password: 'Passwort',
nickname: 'Anzeigename',
login: 'Anmelden',
register: 'Registrieren',
loggingIn: 'Anmeldung...',
generatingKeys: 'Schlüssel werden generiert...',
keysRegenerated: 'Schlüssel generiert und hochgeladen',
registering: 'Registrierung...',
noAccount: 'Kein Konto?',
hasAccount: 'Bereits ein Konto?',
registerLink: 'Registrieren',
loginLink: 'Anmelden',
fillFields: 'Bitte Benutzername und Passwort eingeben',
opFailed: 'Vorgang fehlgeschlagen',
// Tabs
tabChats: 'Chats',
tabContacts: 'Kontakte',
tabDiscover: 'Entdecken',
tabMe: 'Ich',
// Chats
chatsTitle: 'Chats',
searchPlaceholder: 'Suchen',
noChats: 'Keine Chats',
noChatsHint: 'Finde Freunde in den Kontakten und starte einen Chat',
tapToChat: 'Tippen zum Chatten',
encryptedMsg: 'Verschlüsselte Nachricht',
// Chat window
inputPlaceholder: 'Nachricht senden...',
voiceHint: 'Loslassen zum Senden',
sendingVoice: 'Wird gesendet...',
uploading: 'Wird hochgeladen...',
uploadFailed: 'Hochladen fehlgeschlagen',
micFailed: 'Mikrofon nicht verfügbar',
encFailed: 'Verschlüsselung fehlgeschlagen',
sessionFailed: 'Sicherer Kanal konnte nicht hergestellt werden',
imageLabel: '[Bild]',
// Contacts
contactsTitle: 'Kontakte',
friendRequests: 'Freundschaftsanfragen',
searchUsers: 'Benutzername suchen',
noContacts: 'Noch keine Freunde',
noContactsHint: 'Suche nach Benutzernamen, um Freunde hinzuzufügen',
noResults: 'Keine Benutzer gefunden',
searchFailed: 'Suche fehlgeschlagen',
alreadyFriend: 'Bereits befreundet',
add: 'Hinzufügen',
accept: 'Annehmen',
sent: 'Gesendet',
requestSent: 'Freundschaftsanfrage gesendet',
friendAdded: 'Freund hinzugefügt',
opFail: 'Vorgang fehlgeschlagen',
online: 'Online',
discoverTitle: 'Entdecken',
moments: 'Momente',
searchFn: 'Suche',
news: 'Nachrichten',
games: 'Spiele',
nearby: 'Leute in der Nähe',
shopping: 'Einkaufen',
profileTitle: 'Ich',
e2eLabel: 'E2E-Verschlüsselung',
e2eValue: 'X3DH + Double Ratchet',
pqLabel: 'Post-Quantum',
pqValue: 'ML-KEM-768',
keyFingerprint: 'Schlüsselfingerabdruck anzeigen',
changeNickname: 'Anzeigename ändern',
language: 'Sprache',
version: 'Version',
addHomescreen: 'Zum Startbildschirm hinzufügen (iOS)',
logout: 'Abmelden',
logoutConfirm: 'Von PaperPhone abmelden?',
nicknamePrompt: 'Neuen Anzeigenamen eingeben',
nicknameUpdated: 'Anzeigename aktualisiert',
changeAvatar: 'Avatar ändern',
avatarUpdated: 'Avatar aktualisiert',
avatarFailed: 'Avatar-Upload fehlgeschlagen',
updateFailed: 'Aktualisierung fehlgeschlagen',
noKey: 'Kein lokaler Schlüssel gefunden. Bitte erneut anmelden.',
fpLabel: 'Schlüsselfingerabdruck (IK)',
fpWarning: 'Vergleichen Sie Fingerabdrücke mit Ihrem Kontakt, um MITM-Angriffe auszuschließen',
iosInstallTitle: 'Zum Startbildschirm hinzufügen',
iosInstallSteps: '1. Diese Seite in Safari öffnen\n2. Teilen-Button (↑) drücken\n3. "Zum Home-Bildschirm" wählen\n4. "Hinzufügen" tippen\n\nFunktioniert wie eine native App!',
newMessage: 'Neue Nachricht',
newFriendRequest: 'Neue Freundschaftsanfrage erhalten',
friendAccepted: 'Freundschaftsanfrage angenommen',
comingSoon: 'Demnächst verfügbar',
// Calls
callVideo: 'Videoanruf',
callVoice: 'Sprachanruf',
callVideoIncoming: 'Eingehender Videoanruf...',
callVoiceIncoming: 'Eingehender Sprachanruf...',
callGroupIncoming: 'Gruppenanruf beitreten...',
callCalling: 'Anruf läuft...',
callAccept: 'Annehmen',
callAcceptVideo: 'Video',
callAcceptVoice: 'Sprache',
callReject: 'Ablehnen',
callCancel: 'Abbrechen',
callEnd: 'Auflegen',
callMute: 'Stumm',
callUnmute: 'Laut',
callCamera: 'Kamera',
callCameraOff: 'Kamera aus',
callSwitchCam: 'Kamera wechseln',
callSpeaker: 'Lautsprecher',
callBusy: 'Bereits in einem Anruf',
callMediaFailed: 'Zugriff auf Kamera/Mikrofon nicht möglich',
unknownCaller: 'Unbekannter Anrufer',
// Moments
cancel: 'Abbrechen',
newMoment: 'Neuer Beitrag',
publish: 'Veröffentlichen',
addPhoto: 'Foto hinzufügen',
noMoments: 'Keine Beiträge — teile etwas!',
momentPlaceholder: 'Was gibt\'s Neues?',
deleteConfirm: 'Diesen Beitrag löschen?',
sendMoment: 'Wird veröffentlicht...',
// Push
pushNotifications: 'Benachrichtigungen',
pushEnabled: 'Aktiviert',
pushDisabled: 'Deaktiviert',
pushDenied: 'Blockiert',
pushDeniedHint: 'Bitte Benachrichtigungen in den Browsereinstellungen erlauben',
pushTurnedOn: 'Benachrichtigungen aktiviert',
pushTurnedOff: 'Benachrichtigungen deaktiviert',
pushFailed: 'Benachrichtigungen konnten nicht aktiviert werden',
// Devices
devices: 'Angemeldete Geräte',
currentDevice: 'Dieses Gerät',
otherDevices: 'Andere Geräte',
revokeDevice: 'Abmelden',
revokeAllOther: 'Alle anderen Geräte abmelden',
revokeConfirm: 'Dieses Gerät abmelden?',
revokeAllConfirm: 'Alle Geräte außer diesem abmelden?',
sessionRevoked: 'Dieses Gerät wurde abgemeldet',
noOtherDevices: 'Keine anderen aktiven Geräte',
lastActive: 'Zuletzt aktiv',
// Groups
tabGroups: 'Gruppen',
groupsTitle: 'Gruppen',
createGroup: 'Gruppe erstellen',
groupName: 'Gruppenname',
groupNamePlaceholder: 'Gruppennamen eingeben',
selectMembers: 'Mitglieder auswählen',
noGroups: 'Keine Gruppen',
noGroupsHint: 'Erstelle eine Gruppe, um loszulegen',
groupCreated: 'Gruppe erstellt',
groupNotice: 'Gruppenankündigung',
groupNoNotice: 'Keine Ankündigung',
groupMembers: 'Mitglieder',
leaveGroup: 'Gruppe verlassen',
leaveGroupConfirm: 'Diese Gruppe verlassen?',
disbandGroup: 'Gruppe auflösen',
disbandGroupConfirm: 'Diese Gruppe auflösen? Dies kann nicht rückgängig gemacht werden.',
memberLimitReached: 'Mitgliederlimit erreicht (2000)',
groupOwner: 'Inhaber',
groupAdmin: 'Admin',
nMembers: '',
groupInfo: 'Gruppeninfo',
addMembers: 'Mitglieder hinzufügen',
removeMemberConfirm: 'Dieses Mitglied aus der Gruppe entfernen?',
muteGroup: 'Stummschalten',
muteEnabled: 'Benachrichtigungen deaktiviert',
muteDisabled: 'Benachrichtigungen aktiviert',
autoDeleteTitle: 'Nachrichten automatisch löschen',
autoDeleteNever: 'Nie',
autoDelete1d: 'Nach 1 Tag',
autoDelete3d: 'Nach 3 Tagen',
autoDelete7d: 'Nach 1 Woche',
autoDelete30d: 'Nach 1 Monat',
autoDeleteUpdated: 'Auto-Löschung aktualisiert',
autoDeleteOwnerOnly: 'Nur der Inhaber kann dies einstellen',
// Tags
tags: 'Tags',
manageTags: 'Tags verwalten',
newTag: 'Neuer Tag',
editTag: 'Tag bearbeiten',
deleteTag: 'Tag löschen',
tagColor: 'Tag-Farbe',
tagName: 'Tag-Name',
tagNamePlaceholder: 'Tag-Namen eingeben',
tagDelConfirm: 'Diesen Tag löschen?',
noTags: 'Keine Tags',
allContacts: 'Alle',
setTags: 'Tags setzen',
tagExists: 'Tag existiert bereits',
tagCreated: 'Tag erstellt',
tagUpdated: 'Tag aktualisiert',
tagDeleted: 'Tag gelöscht',
// Visibility
whoCanSee: 'Wer kann das sehen',
visibilityPublic: 'Öffentlich',
visibilityPublicDesc: 'Für alle Freunde sichtbar',
visibilityWhitelist: 'Auswahl',
visibilityWhitelistDesc: 'Nur für ausgewählte Freunde/Tags sichtbar',
visibilityBlacklist: 'Ausschluss',
visibilityBlacklistDesc: 'Für ausgewählte Freunde/Tags ausgeblendet',
selectTags: 'Tags auswählen',
selectFriends: 'Freunde auswählen',
selectedCount: 'ausgewählt',
confirm: 'OK',
// Likes
likedBy: 'Gefällt mir',
likedUsers: 'Gefällt',
nLikes: 'Likes',
},
ru: {
// App
appName: 'PaperPhone',
appTagline: 'Сквозное шифрование · Прямая секретность · Пост-квантовый',
keyNotice: 'Ключи хранятся только на этом устройстве',
// Auth
username: 'Имя пользователя',
password: 'Пароль',
nickname: 'Псевдоним',
login: 'Войти',
register: 'Регистрация',
loggingIn: 'Вход...',
generatingKeys: 'Генерация ключей...',
keysRegenerated: 'Ключи сгенерированы и загружены',
registering: 'Регистрация...',
noAccount: 'Нет аккаунта?',
hasAccount: 'Уже есть аккаунт?',
registerLink: 'Регистрация',
loginLink: 'Войти',
fillFields: 'Введите имя пользователя и пароль',
opFailed: 'Операция не удалась',
// Tabs
tabChats: 'Чаты',
tabContacts: 'Контакты',
tabDiscover: 'Поиск',
tabMe: 'Я',
// Chats
chatsTitle: 'Чаты',
searchPlaceholder: 'Поиск',
noChats: 'Нет чатов',
noChatsHint: 'Найдите друзей в контактах, чтобы начать переписку',
tapToChat: 'Нажмите, чтобы начать',
encryptedMsg: 'Зашифрованное сообщение',
// Chat window
inputPlaceholder: 'Написать сообщение...',
voiceHint: 'Отпустите для отправки',
sendingVoice: 'Отправка...',
uploading: 'Загрузка...',
uploadFailed: 'Не удалось загрузить',
micFailed: 'Нет доступа к микрофону',
encFailed: 'Ошибка шифрования',
sessionFailed: 'Не удалось установить защищённый канал',
imageLabel: '[Изображение]',
// Contacts
contactsTitle: 'Контакты',
friendRequests: 'Запросы в друзья',
searchUsers: 'Поиск по имени',
noContacts: 'Друзей пока нет',
noContactsHint: 'Найдите пользователей, чтобы добавить в друзья',
noResults: 'Пользователи не найдены',
searchFailed: 'Ошибка поиска',
alreadyFriend: 'Уже в друзьях',
add: 'Добавить',
accept: 'Принять',
sent: 'Отправлено',
requestSent: 'Запрос отправлен',
friendAdded: 'Друг добавлен',
opFail: 'Операция не удалась',
online: 'В сети',
discoverTitle: 'Поиск',
moments: 'Моменты',
searchFn: 'Поиск',
news: 'Новости',
games: 'Игры',
nearby: 'Люди рядом',
shopping: 'Покупки',
profileTitle: 'Я',
e2eLabel: 'Сквозное шифрование',
e2eValue: 'X3DH + Double Ratchet',
pqLabel: 'Пост-квантовый',
pqValue: 'ML-KEM-768',
keyFingerprint: 'Показать отпечаток ключа',
changeNickname: 'Изменить псевдоним',
language: 'Язык',
version: 'Версия',
addHomescreen: 'Добавить на рабочий стол (iOS)',
logout: 'Выйти',
logoutConfirm: 'Выйти из PaperPhone?',
nicknamePrompt: 'Введите новый псевдоним',
nicknameUpdated: 'Псевдоним обновлён',
changeAvatar: 'Сменить аватар',
avatarUpdated: 'Аватар обновлён',
avatarFailed: 'Не удалось загрузить аватар',
updateFailed: 'Ошибка обновления',
noKey: 'Ключ не найден. Пожалуйста, войдите снова.',
fpLabel: 'Отпечаток ключа (IK)',
fpWarning: 'Сравните отпечатки с контактом, чтобы исключить атаку MITM',
iosInstallTitle: 'Добавить на рабочий стол',
iosInstallSteps: '1. Откройте эту страницу в Safari\n2. Нажмите кнопку «Поделиться» (↑)\n3. Выберите «На экран Домой»\n4. Нажмите «Добавить»\n\nРаботает как нативное приложение!',
newMessage: 'Новое сообщение',
newFriendRequest: 'Новый запрос в друзья',
friendAccepted: 'Запрос в друзья принят',
comingSoon: 'Скоро',
// Calls
callVideo: 'Видеозвонок',
callVoice: 'Голосовой звонок',
callVideoIncoming: 'Входящий видеозвонок...',
callVoiceIncoming: 'Входящий голосовой звонок...',
callGroupIncoming: 'Присоединение к группе...',
callCalling: 'Вызов...',
callAccept: 'Ответить',
callAcceptVideo: 'Видео',
callAcceptVoice: 'Голос',
callReject: 'Отклонить',
callCancel: 'Отмена',
callEnd: 'Завершить',
callMute: 'Без звука',
callUnmute: 'Со звуком',
callCamera: 'Камера',
callCameraOff: 'Камера выкл.',
callSwitchCam: 'Переключить',
callSpeaker: 'Динамик',
callBusy: 'Уже идёт вызов',
callMediaFailed: 'Нет доступа к камере/микрофону',
unknownCaller: 'Неизвестный',
// Moments
cancel: 'Отмена',
newMoment: 'Новый пост',
publish: 'Опубликовать',
addPhoto: 'Добавить фото',
noMoments: 'Пока нет постов — поделитесь чем-нибудь!',
momentPlaceholder: 'Что нового?',
deleteConfirm: 'Удалить этот пост?',
sendMoment: 'Публикация...',
// Push
pushNotifications: 'Уведомления',
pushEnabled: 'Включены',
pushDisabled: 'Отключены',
pushDenied: 'Заблокированы',
pushDeniedHint: 'Разрешите уведомления в настройках браузера',
pushTurnedOn: 'Уведомления включены',
pushTurnedOff: 'Уведомления отключены',
pushFailed: 'Не удалось включить уведомления',
// Devices
devices: 'Активные сессии',
currentDevice: 'Это устройство',
otherDevices: 'Другие устройства',
revokeDevice: 'Выйти',
revokeAllOther: 'Выйти на всех других устройствах',
revokeConfirm: 'Выйти на этом устройстве?',
revokeAllConfirm: 'Выйти на всех устройствах, кроме этого?',
sessionRevoked: 'Сессия завершена',
noOtherDevices: 'Нет других активных устройств',
lastActive: 'Последняя активность',
// Groups
tabGroups: 'Группы',
groupsTitle: 'Группы',
createGroup: 'Создать группу',
groupName: 'Название группы',
groupNamePlaceholder: 'Введите название группы',
selectMembers: 'Выбрать участников',
noGroups: 'Нет групп',
noGroupsHint: 'Создайте группу, чтобы начать общение',
groupCreated: 'Группа создана',
groupNotice: 'Объявление группы',
groupNoNotice: 'Нет объявлений',
groupMembers: 'Участники',
leaveGroup: 'Покинуть группу',
leaveGroupConfirm: 'Покинуть эту группу?',
disbandGroup: 'Расформировать группу',
disbandGroupConfirm: 'Расформировать группу? Это действие нельзя отменить.',
memberLimitReached: 'Достигнут лимит участников (2000)',
groupOwner: 'Владелец',
groupAdmin: 'Админ',
nMembers: '',
groupInfo: 'Информация о группе',
addMembers: 'Добавить участников',
removeMemberConfirm: 'Удалить этого участника из группы?',
muteGroup: 'Без звука',
muteEnabled: 'Уведомления отключены',
muteDisabled: 'Уведомления включены',
autoDeleteTitle: 'Автоудаление сообщений',
autoDeleteNever: 'Никогда',
autoDelete1d: 'Через 1 день',
autoDelete3d: 'Через 3 дня',
autoDelete7d: 'Через 1 неделю',
autoDelete30d: 'Через 1 месяц',
autoDeleteUpdated: 'Автоудаление обновлено',
autoDeleteOwnerOnly: 'Только владелец может настроить',
// Tags
tags: 'Теги',
manageTags: 'Управление тегами',
newTag: 'Новый тег',
editTag: 'Изменить тег',
deleteTag: 'Удалить тег',
tagColor: 'Цвет тега',
tagName: 'Название тега',
tagNamePlaceholder: 'Введите название тега',
tagDelConfirm: 'Удалить этот тег?',
noTags: 'Нет тегов',
allContacts: 'Все',
setTags: 'Установить теги',
tagExists: 'Тег уже существует',
tagCreated: 'Тег создан',
tagUpdated: 'Тег обновлён',
tagDeleted: 'Тег удалён',
// Visibility
whoCanSee: 'Кто может видеть',
visibilityPublic: 'Все',
visibilityPublicDesc: 'Видно всем друзьям',
visibilityWhitelist: 'Выборочно',
visibilityWhitelistDesc: 'Только выбранные друзья/теги',
visibilityBlacklist: 'Исключить',
visibilityBlacklistDesc: 'Скрыть от выбранных друзей/тегов',
selectTags: 'Выбрать теги',
selectFriends: 'Выбрать друзей',
selectedCount: 'выбрано',
confirm: 'ОК',
// Likes
likedBy: 'Лайк',
likedUsers: 'Понравилось',
nLikes: 'лайков',
},
es: {
// App
appName: 'PaperPhone',
appTagline: 'Cifrado extremo a extremo · Secreto hacia adelante · Post-cuántico',
keyNotice: 'Las claves se almacenan solo en este dispositivo',
// Auth
username: 'Usuario',
password: 'Contraseña',
nickname: 'Apodo',
login: 'Iniciar sesión',
register: 'Registrarse',
loggingIn: 'Iniciando sesión...',
generatingKeys: 'Generando claves...',
keysRegenerated: 'Claves generadas y subidas',
registering: 'Registrando...',
noAccount: '¿No tienes cuenta?',
hasAccount: '¿Ya tienes cuenta?',
registerLink: 'Registrarse',
loginLink: 'Iniciar sesión',
fillFields: 'Ingresa usuario y contraseña',
opFailed: 'Operación fallida',
// Tabs
tabChats: 'Chats',
tabContacts: 'Contactos',
tabDiscover: 'Descubrir',
tabMe: 'Yo',
// Chats
chatsTitle: 'Chats',
searchPlaceholder: 'Buscar',
noChats: 'Sin conversaciones',
noChatsHint: 'Busca amigos en contactos para iniciar un chat',
tapToChat: 'Toca para chatear',
encryptedMsg: 'Mensaje cifrado',
// Chat window
inputPlaceholder: 'Enviar mensaje...',
voiceHint: 'Suelta para enviar',
sendingVoice: 'Enviando...',
uploading: 'Subiendo...',
uploadFailed: 'Error al subir',
micFailed: 'No se puede acceder al micrófono',
encFailed: 'Error de cifrado',
sessionFailed: 'No se pudo establecer el canal seguro',
imageLabel: '[Imagen]',
// Contacts
contactsTitle: 'Contactos',
friendRequests: 'Solicitudes de amistad',
searchUsers: 'Buscar por nombre de usuario',
noContacts: 'Aún no tienes amigos',
noContactsHint: 'Busca usuarios para agregar amigos',
noResults: 'No se encontraron usuarios',
searchFailed: 'Error en la búsqueda',
alreadyFriend: 'Ya son amigos',
add: 'Agregar',
accept: 'Aceptar',
sent: 'Enviado',
requestSent: 'Solicitud enviada',
friendAdded: 'Amigo agregado',
opFail: 'Operación fallida',
online: 'En línea',
discoverTitle: 'Descubrir',
moments: 'Momentos',
searchFn: 'Buscar',
news: 'Noticias',
games: 'Juegos',
nearby: 'Personas cercanas',
shopping: 'Compras',
profileTitle: 'Yo',
e2eLabel: 'Cifrado E2E',
e2eValue: 'X3DH + Double Ratchet',
pqLabel: 'Post-Cuántico',
pqValue: 'ML-KEM-768',
keyFingerprint: 'Ver huella digital de la clave',
changeNickname: 'Cambiar apodo',
language: 'Idioma',
version: 'Versión',
addHomescreen: 'Añadir a pantalla de inicio (iOS)',
logout: 'Cerrar sesión',
logoutConfirm: '¿Cerrar sesión en PaperPhone?',
nicknamePrompt: 'Ingresa un nuevo apodo',
nicknameUpdated: 'Apodo actualizado',
changeAvatar: 'Cambiar avatar',
avatarUpdated: 'Avatar actualizado',
avatarFailed: 'Error al subir avatar',
updateFailed: 'Error al actualizar',
noKey: 'No se encontró clave local. Por favor, inicia sesión de nuevo.',
fpLabel: 'Huella digital de la clave (IK)',
fpWarning: 'Compara las huellas con tu contacto para verificar que no haya ataques MITM',
iosInstallTitle: 'Añadir a pantalla de inicio',
iosInstallSteps: '1. Abre esta página en Safari\n2. Toca el botón Compartir (↑)\n3. Selecciona "Añadir a inicio"\n4. Toca "Añadir"\n\n¡Funciona como una app nativa!',
newMessage: 'Nuevo mensaje',
newFriendRequest: 'Nueva solicitud de amistad',
friendAccepted: 'Solicitud de amistad aceptada',
comingSoon: 'Próximamente',
// Calls
callVideo: 'Videollamada',
callVoice: 'Llamada de voz',
callVideoIncoming: 'Videollamada entrante...',
callVoiceIncoming: 'Llamada de voz entrante...',
callGroupIncoming: 'Unirse a la llamada grupal...',
callCalling: 'Llamando...',
callAccept: 'Contestar',
callAcceptVideo: 'Video',
callAcceptVoice: 'Voz',
callReject: 'Rechazar',
callCancel: 'Cancelar',
callEnd: 'Colgar',
callMute: 'Silenciar',
callUnmute: 'Activar sonido',
callCamera: 'Cámara',
callCameraOff: 'Cámara apagada',
callSwitchCam: 'Cambiar cámara',
callSpeaker: 'Altavoz',
callBusy: 'Ya hay una llamada en curso',
callMediaFailed: 'No se puede acceder a la cámara/micrófono',
unknownCaller: 'Llamante desconocido',
// Moments
cancel: 'Cancelar',
newMoment: 'Nueva publicación',
publish: 'Publicar',
addPhoto: 'Añadir foto',
noMoments: 'Sin publicaciones — ¡comparte algo!',
momentPlaceholder: '¿Qué hay de nuevo?',
deleteConfirm: '¿Eliminar esta publicación?',
sendMoment: 'Publicando...',
// Push
pushNotifications: 'Notificaciones',
pushEnabled: 'Activadas',
pushDisabled: 'Desactivadas',
pushDenied: 'Bloqueadas',
pushDeniedHint: 'Permite las notificaciones en la configuración del navegador',
pushTurnedOn: 'Notificaciones activadas',
pushTurnedOff: 'Notificaciones desactivadas',
pushFailed: 'No se pudieron activar las notificaciones',
// Devices
devices: 'Dispositivos conectados',
currentDevice: 'Este dispositivo',
otherDevices: 'Otros dispositivos',
revokeDevice: 'Cerrar sesión',
revokeAllOther: 'Cerrar sesión en todos los demás dispositivos',
revokeConfirm: '¿Cerrar sesión en este dispositivo?',
revokeAllConfirm: '¿Cerrar sesión en todos los dispositivos excepto este?',
sessionRevoked: 'Sesión cerrada en este dispositivo',
noOtherDevices: 'No hay otros dispositivos activos',
lastActive: 'Última actividad',
// Groups
tabGroups: 'Grupos',
groupsTitle: 'Grupos',
createGroup: 'Crear grupo',
groupName: 'Nombre del grupo',
groupNamePlaceholder: 'Ingresa el nombre del grupo',
selectMembers: 'Seleccionar miembros',
noGroups: 'Sin grupos',
noGroupsHint: 'Crea un grupo para empezar a chatear',
groupCreated: 'Grupo creado',
groupNotice: 'Anuncio del grupo',
groupNoNotice: 'Sin anuncios',
groupMembers: 'Miembros',
leaveGroup: 'Abandonar grupo',
leaveGroupConfirm: '¿Abandonar este grupo?',
disbandGroup: 'Disolver grupo',
disbandGroupConfirm: '¿Disolver este grupo? Esta acción es irreversible.',
memberLimitReached: 'Límite de miembros alcanzado (2000)',
groupOwner: 'Propietario',
groupAdmin: 'Administrador',
nMembers: '',
groupInfo: 'Info del grupo',
addMembers: 'Añadir miembros',
removeMemberConfirm: '¿Eliminar a este miembro del grupo?',
muteGroup: 'Silenciar',
muteEnabled: 'Notificaciones desactivadas',
muteDisabled: 'Notificaciones activadas',
autoDeleteTitle: 'Eliminación automática de mensajes',
autoDeleteNever: 'Nunca',
autoDelete1d: 'Después de 1 día',
autoDelete3d: 'Después de 3 días',
autoDelete7d: 'Después de 1 semana',
autoDelete30d: 'Después de 1 mes',
autoDeleteUpdated: 'Eliminación automática actualizada',
autoDeleteOwnerOnly: 'Solo el propietario puede configurar',
// Tags
tags: 'Etiquetas',
manageTags: 'Gestionar etiquetas',
newTag: 'Nueva etiqueta',
editTag: 'Editar etiqueta',
deleteTag: 'Eliminar etiqueta',
tagColor: 'Color de etiqueta',
tagName: 'Nombre de etiqueta',
tagNamePlaceholder: 'Ingresa el nombre de la etiqueta',
tagDelConfirm: '¿Eliminar esta etiqueta?',
noTags: 'Sin etiquetas',
allContacts: 'Todos',
setTags: 'Establecer etiquetas',
tagExists: 'La etiqueta ya existe',
tagCreated: 'Etiqueta creada',
tagUpdated: 'Etiqueta actualizada',
tagDeleted: 'Etiqueta eliminada',
// Visibility
whoCanSee: 'Quién puede ver',
visibilityPublic: 'Público',
visibilityPublicDesc: 'Visible para todos los amigos',
visibilityWhitelist: 'Selección',
visibilityWhitelistDesc: 'Visible para amigos/etiquetas seleccionados',
visibilityBlacklist: 'Exclusión',
visibilityBlacklistDesc: 'Oculto para amigos/etiquetas seleccionados',
selectTags: 'Seleccionar etiquetas',
selectFriends: 'Seleccionar amigos',
selectedCount: 'seleccionados',
confirm: 'OK',
// Likes
likedBy: 'Me gusta',
likedUsers: 'Les gusta',
nLikes: 'me gusta',
},
};
// ── Language Engine ────────────────────────────────────────────────────────
const SUPPORTED = ['zh', 'en', 'ja', 'ko', 'fr'];
const LANG_NAMES = { zh: '中文', en: 'English', ja: '日本語', ko: '한국어', fr: 'Français' };
const LANG_FLAGS = { zh: '🇨🇳', en: '🇺🇸', ja: '🇯🇵', ko: '🇰🇷', fr: '🇫🇷' };
const SUPPORTED = ['zh', 'en', 'ja', 'ko', 'fr', 'de', 'ru', 'es'];
const LANG_NAMES = { zh: '中文', en: 'English', ja: '日本語', ko: '한국어', fr: 'Français', de: 'Deutsch', ru: 'Русский', es: 'Español' };
const LANG_FLAGS = { zh: '🇨🇳', en: '🇺🇸', ja: '🇯🇵', ko: '🇰🇷', fr: '🇫🇷', de: '🇩🇪', ru: '🇷🇺', es: '🇪🇸' };
function detectLang() {
const saved = localStorage.getItem('pp_lang');
@@ -1102,6 +1744,9 @@ function detectLang() {
if (nav.startsWith('ja')) return 'ja';
if (nav.startsWith('ko')) return 'ko';
if (nav.startsWith('fr')) return 'fr';
if (nav.startsWith('de')) return 'de';
if (nav.startsWith('ru')) return 'ru';
if (nav.startsWith('es')) return 'es';
return 'en';
}

BIN
ui.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 160 KiB