# yaml-language-server: $schema=https://schema.zeabur.app/template.json
apiVersion: zeabur.com/v1
kind: Template
metadata:
    name: PaperPhone-Plus
spec:
    description: A WeChat-style, end-to-end encrypted IM app built with React + Rust (Axum), featuring stateless ECDH E2EE, group E2E encryption (Signal-style Sender Keys), LiveKit SFU voice/video calls (1:1 and meetings up to 100 participants), real-time voice changer, reliable cursor-based message synchronization, durable device sessions, SOCKS5/HTTP/HTTPS proxy support, Cloudflare R2 file storage, 8-language i18n, and iOS PWA support — fully self-hostable.
    icon: https://raw.githubusercontent.com/619dev/Paperphone-plus/main/client/public/icons/icon-512.png
    variables:
        - key: PUBLIC_DOMAIN
          type: DOMAIN
          name: Client Domain
          description: The public domain for the PaperPhonePlus web client (e.g. paperphoneplus.example.com).
        - key: JWT_SECRET
          type: STRING
          name: JWT Secret
          description: A long random string used to sign authentication tokens. Change this before going to production.
        - key: LIVEKIT_DOMAIN
          type: DOMAIN
          name: LiveKit Meeting Domain
          description: Public domain for the LiveKit WebSocket/API service.
        - key: LIVEKIT_API_KEY
          type: STRING
          name: LiveKit API Key
          description: API key shared by the PaperPhone server and LiveKit service. Change the default in production.
        - key: LIVEKIT_API_SECRET
          type: STRING
          name: LiveKit API Secret
          description: A random secret of at least 32 characters shared by server and LiveKit.
        - key: R2_ACCOUNT_ID
          type: STRING
          name: Cloudflare R2 Account ID
          description: Your Cloudflare account ID (found in Cloudflare Dashboard → right sidebar).
        - key: R2_ACCESS_KEY_ID
          type: STRING
          name: R2 Access Key ID
          description: R2 API token access key (R2 → Manage API tokens → Create token).
        - key: R2_SECRET_ACCESS_KEY
          type: STRING
          name: R2 Secret Access Key
          description: R2 API token secret key paired with the access key above.
        - key: R2_BUCKET
          type: STRING
          name: R2 Bucket Name
          description: Name of the R2 bucket to store uploaded files (e.g. paperphoneplus).
        - key: R2_PUBLIC_URL
          type: STRING
          name: R2 Public URL (optional)
          description: Public base URL for the bucket, e.g. https://pub-xxx.r2.dev or your custom domain. If set, files are served directly from R2/CDN. Leave blank to proxy files through the server.
        - key: MYSQL_ROOT_PASSWORD
          type: STRING
          name: MySQL Root Password
          description: Password for the MySQL root user. Set this yourself — it will NOT be auto-generated.
        - key: REDIS_PASS
          type: STRING
          name: Redis Password
          description: A strong password shared by Redis, PaperPhone server, and LiveKit.
        - key: VAPID_PUBLIC_KEY
          type: STRING
          name: VAPID Public Key (optional)
          description: 'Web Push VAPID public key for push notifications. Generate with: npx web-push generate-vapid-keys. Leave blank to disable push.'
        - key: VAPID_PRIVATE_KEY
          type: STRING
          name: VAPID Private Key (optional)
          description: Web Push VAPID private key paired with the public key above.
        - key: VAPID_SUBJECT
          type: STRING
          name: VAPID Subject (optional)
          description: Contact email for VAPID, e.g. mailto:admin@yoursite.com.
        - key: ONESIGNAL_APP_ID
          type: STRING
          name: OneSignal App ID (optional)
          description: For native Android/iOS push via Median.co. Get from OneSignal Dashboard → Settings → Keys & IDs.
        - key: ONESIGNAL_REST_KEY
          type: STRING
          name: OneSignal REST API Key (optional)
          description: OneSignal REST API key paired with the App ID above.
        - key: ONESIGNAL_RELAY_SECRET
          type: STRING
          name: OneSignal Relay Secret (optional)
          description: Set this on the app developer's server to enable the OneSignal push relay endpoint. Self-hosted servers use this secret to authenticate. Generate a long random string. Leave blank to disable the relay.
        - key: ONESIGNAL_RELAY_URL
          type: STRING
          name: OneSignal Relay URL (optional)
          description: For self-hosted servers only. URL of the push relay service (the app developer's server), e.g. https://api.paperphoneplus.app. Leave blank if you have local OneSignal credentials configured.
        - key: ONESIGNAL_RELAY_KEY
          type: STRING
          name: OneSignal Relay Key (optional)
          description: For self-hosted servers only. The shared secret to authenticate with the push relay. Must match the relay server's ONESIGNAL_RELAY_SECRET.
        - key: TELEGRAM_BOT_TOKEN
          type: STRING
          name: Telegram Bot Token (optional)
          description: Token from @BotFather for proxying Telegram sticker packs. Leave blank to disable sticker packs.
        - key: STICKER_PACKS
          type: STRING
          name: Sticker Packs (optional)
          description: Comma-separated list of Telegram sticker pack names with labels, in the format name:label (e.g. pack_name:My Pack,other_pack:Other Pack). No limit on quantity. Leave blank to use the 13 built-in defaults.
        - key: FCM_PROJECT_ID
          type: STRING
          name: FCM Project ID (optional)
          description: Firebase project ID for native Android push (Capacitor). Found in Firebase Console → Project Settings → General. Leave blank to disable FCM.
        - key: FCM_CLIENT_EMAIL
          type: STRING
          name: FCM Client Email (optional)
          description: Firebase service account email from the service account JSON key file. Found in Firebase Console → Project Settings → Service accounts.
        - key: FCM_PRIVATE_KEY
          type: STRING
          name: FCM Private Key (optional)
          description: RSA private key from the Firebase service account JSON key file. Copy the full "private_key" field value including BEGIN/END markers.
        - key: FCM_RELAY_SECRET
          type: STRING
          name: FCM Relay Secret (optional)
          description: Set this on the app developer's server to enable the FCM push relay endpoint. Self-hosted servers use this secret to authenticate. Generate a long random string. Leave blank to disable the relay.
        - key: FCM_RELAY_URL
          type: STRING
          name: FCM Relay URL (optional)
          description: For self-hosted servers only. URL of the push relay service (the app developer's server), e.g. https://api.paperphoneplus.app. Leave blank if you have local FCM credentials configured.
        - key: FCM_RELAY_KEY
          type: STRING
          name: FCM Relay Key (optional)
          description: For self-hosted servers only. The shared secret to authenticate with the push relay. Must match the relay server's FCM_RELAY_SECRET.
        - key: NTFY_BASE_URL
          type: STRING
          name: ntfy Server URL (optional)
          description: Base URL for ntfy push notifications (for Chinese Android devices without Google Services). Defaults to https://ntfy.sh public service. Set to your own ntfy server URL if self-hosting.
        - key: NTFY_TOKEN
          type: STRING
          name: ntfy Auth Token (optional)
          description: Authentication token for self-hosted ntfy servers. Leave blank when using the public ntfy.sh service.
        - key: APNS_TEAM_ID
          type: STRING
          name: APNS Team ID (optional)
          description: Apple Developer Team ID for iOS native push notifications. Found in Apple Developer account → Membership page (10-char alphanumeric). Leave blank to disable APNS.
        - key: APNS_KEY_ID
          type: STRING
          name: APNS Key ID (optional)
          description: Key ID of the APNS authentication key (.p8 file). Found in Apple Developer → Keys → your key (10-char alphanumeric).
        - key: APNS_PRIVATE_KEY
          type: STRING
          name: APNS Private Key (optional)
          description: Contents of the .p8 private key file from Apple Developer → Keys. Include the full PEM content (BEGIN/END markers). Replace real newlines with \n when pasting into environment variables.
        - key: APNS_BUNDLE_ID
          type: STRING
          name: APNS Bundle ID (optional)
          description: The iOS app Bundle Identifier, e.g. com.paperphoneplus.app. Must match the Bundle ID configured in your Xcode project and Apple Developer portal.
        - key: APNS_SANDBOX
          type: STRING
          name: APNS Sandbox Mode (optional)
          description: Set to "true" for development/TestFlight builds, "false" for App Store production builds. Defaults to "false".
        - key: APNS_RELAY_SECRET
          type: STRING
          name: APNS Relay Secret (optional)
          description: Set this on the app developer's server to enable the push relay endpoint. Self-hosted servers use this secret to authenticate. Generate a long random string. Leave blank to disable the relay.
        - key: APNS_RELAY_URL
          type: STRING
          name: APNS Relay URL (optional)
          description: For self-hosted servers only. URL of the push relay service (the app developer's server), e.g. https://api.paperphoneplus.app. Leave blank if you have local APNS credentials configured.
        - key: APNS_RELAY_KEY
          type: STRING
          name: APNS Relay Key (optional)
          description: For self-hosted servers only. The shared secret to authenticate with the push relay. Must match the relay server's APNS_RELAY_SECRET.
        - key: ADMIN_PASSWORD
          type: STRING
          name: Admin Panel Password
          description: 'Password for accessing the built-in admin panel at /admin. Change this to a strong password in production. Default: admin123.'
        - key: ADMIN_PATH
          type: STRING
          name: Admin Panel Path (optional)
          description: 'Custom URL path for the admin panel. Default: /admin. Change this for security through obscurity.'
    tags:
        - Chat
        - Security
        - Communication
    readme: "# PaperPhonePlus\n\nA **WeChat-style**, end-to-end encrypted instant messaging application.\nRewritten with **React 19 + TypeScript + Vite** frontend and **Rust (Axum)** backend\nfor maximum performance and type safety.\n\n## Security\n- **Stateless ECDH + XSalsa20-Poly1305** — per-message ephemeral keys, forward secrecy without session state\n- **Zero-knowledge server** — only ciphertext is stored; private keys never leave the device\n- **Four-tier key persistence** — memory → localStorage → sessionStorage → IndexedDB, survives WebView restarts\n- **Ciphertext-only optimistic sends (v2.3.5)** — outgoing private messages are encrypted before entering the persistent offline cache\n\n## Features\n- \U0001F4F9 **Video & Voice Calls** — LiveKit SFU for both 1:1 calls and meetings (up to 100 participants, host mute-all and lecture mode)\n- \U0001F399️ **Voice Changer** — real-time voice effects for voice messages, 1:1 calls & group calls with 3 modes (0.8x deep / 1.0x normal / 1.2x high-pitched), powered by Web Audio API\n- ⏱️ **Safe Voice Recording** — 120-second automatic limit, wake lock while recording, and reliable microphone cleanup on page exit\n- \U0001F4F1 **Session Persistence** — 30-minute access tokens with 90-day device refresh sessions; VPN/IP/network changes reconnect without asking for a password\n- \U0001F4E8 **Reliable Message Sync** — bidirectional heartbeat, persistent outbox, idempotent message IDs, and server-sequence cursor catch-up\n- \U0001F4F4 **Offline Access** — account-isolated contacts, groups, chats, Moments, Timeline, and media cache; offline sends retry automatically\n- \U0001F50E **Unicode Friend Search** — IME-safe, NFC-normalized Chinese username and nickname search\n- \U0001F465 **Group Chat** — up to 2000 members, switchable \"Encrypted\" / \"Unencrypted\" modes (owner-only toggle, switching clears chat history). Encrypted mode uses Signal-style Sender Key protocol (XSalsa20-Poly1305 + ECDH key distribution) — only members can decrypt; bots disabled in encrypted mode. DND mode, member management\n- \U0001F310 **Moments** — social feed with text + up to 9 photos or 1 video (≤ 10 min), likes, comments, tag-based visibility\n- \U0001F464 **User Profile** — contact profile page with Moments feed, bidirectional privacy controls (hide their / hide mine)\n- \U0001F4F0 **Timeline** — Xiaohongshu-style public feed with masonry layout, up to 50 images/videos + 2000-char text, anonymous posting, likes & comments\n- ⏱️ **Auto-Delete Messages** — 5 tiers: never / 1 day / 3 days / 1 week / 1 month\n- \U0001F514 **Push Notifications** — Web Push (VAPID) + FCM + OneSignal + ntfy + APNS five-channel (iOS native + Chinese Android without Google Services supported)\n- \U0001F310 **Multi-language** — Chinese, English, Japanese, Korean, French, German, Russian, Spanish\n- \U0001F4F1 **iOS PWA** — \"Add to Home Screen\" via Safari, no enterprise cert needed\n- \U0001F4AC Rich messaging: text, images, video, document files (PDF/DOCX/XLSX etc. with type icons), voice, 200+ emoji (8 categories), Telegram sticker packs, delivery receipts, typing indicators\n- \U0001F5C2️ **Cloudflare R2** file storage (images, voice & video)\n- \U0001F3F7️ **Friend Tags** — tag-based contact filtering & Moments visibility control\n- \U0001F3AD **Telegram Sticker Packs** — dynamic sticker packs with configurable pack list (13 built-in defaults), unlimited quantity\n- \U0001F511 **Two-Factor Auth (2FA)** — Google Authenticator TOTP, 8 recovery codes\n- \U0001F4F7 **QR Code Scan & Share** — scan QR to add friends or join groups; group QR codes with configurable expiry (1 week / 1 month / 3 months)\n- \U0001F46B **Friend System** — friend requests require approval with up to 512-char message; custom nicknames; multi-tag grouping\n- \U0001F4E4 **File Upload** — up to 500MB per file, Cloudflare R2 or local storage, with progress animation\n- \U0001F310 **Proxy Settings** — SOCKS5 / HTTP / HTTPS proxy support, configurable on login and settings pages (address, port, username, password)\n- \U0001F6E1️ **Content Moderation** — User reporting (6 reason categories) + user blocking (instantly hides posts/messages) + Terms of Use (EULA)\n- \U0001F527 **Admin Panel** — Embedded web dashboard (`/admin`, path customizable via `ADMIN_PATH`), password-protected, review reports, delete content, ban users — 8 languages\n\n## After Deployment\n1. Open the domain assigned to the **client** service.\n2. Register an account — cryptographic keys are generated locally on your device.\n3. (Optional) Configure Cloudflare R2 for image/voice/video uploads — set `R2_*` variables on the **server** service.\n4. (Optional) Enable push notifications — set `VAPID_*` variables for Web Push, `FCM_*` for native Android (Capacitor), `APNS_*` for native iOS.\n5. (Optional) Enable Telegram sticker packs — set `TELEGRAM_BOT_TOKEN` and optionally `STICKER_PACKS`.\n6. (Optional) Access the admin panel at `/admin` (or custom `ADMIN_PATH`) to moderate user reports. Set `ADMIN_PASSWORD` for production security.\n7. After an upgrade, inspect the **server** logs and confirm it becomes healthy before updating clients. Test a VPN/network switch and an offline send to verify automatic recovery and exactly-once storage.\n8. For v2.3.5, send a private message while briefly offline and confirm it retries after reconnecting; pending Web messages persist ciphertext only.\n\nMySQL is started with `utf8mb4` and `utf8mb4_unicode_ci`. On startup, the server automatically migrates and verifies the message sequence, idempotency, refresh-session, and searchable-character-set fields. **For upgrades, redeploy the server first and wait for it to become healthy before updating clients. Back up MySQL before production upgrades.**\n\n## LiveKit calls on Zeabur\nThis template deploys a LiveKit service with WebSocket/API (7880) and ICE/TCP\nfallback (7881). Zeabur currently does not expose UDP service ports, so calls\ncan connect over TCP but may have higher latency or reduced quality on weak networks.\nThe template keeps the LiveKit configuration ready for UDP 7882; expose it when\nZeabur adds UDP support. For production-quality 100-person meetings today, use\nLiveKit Cloud or deploy LiveKit on a VM with UDP 7882 and set `LIVEKIT_URL` on server.\n\n## Tip: Zeabur + Vercel Hybrid\nYou can delete the deployed **client** service on Zeabur and use **Vercel** to host the frontend instead.\nThis gives you Vercel’s global CDN for the frontend while Zeabur handles server/MySQL/Redis.\nJust set `VITE_API_URL` in Vercel to the public domain of this server service.\n\n## Docker Hub Images\n- `facilisvelox/paperphone-plus-client:latest`\n- `facilisvelox/paperphone-plus-server:latest`"
    services:
        - name: MySQL
          icon: https://raw.githubusercontent.com/zeabur/service-icons/main/marketplace/mysql.svg
          template: PREBUILT
          spec:
            id: mysql
            source:
                image: mysql:8.0
                command:
                    - mysqld
                    - --character-set-server=utf8mb4
                    - --collation-server=utf8mb4_unicode_ci
            ports:
                - id: database
                  port: 3306
                  type: TCP
            volumes:
                - id: data
                  dir: /var/lib/mysql
            instructions:
                - title: MySQL Host
                  content: ${PORT_FORWARDED_HOSTNAME}
                - title: MySQL Port
                  content: ${DATABASE_PORT_FORWARDED_PORT}
                - title: MySQL User
                  content: ${MYSQL_USER}
                - title: MySQL Password
                  content: ${MYSQL_PASSWORD}
                - title: MySQL Database
                  content: ${MYSQL_DATABASE}
            env:
                MYSQL_DATABASE:
                    default: paperphone
                    expose: true
                MYSQL_HOST:
                    default: ${CONTAINER_HOSTNAME}
                    expose: true
                MYSQL_PASSWORD:
                    default: ${MYSQL_ROOT_PASSWORD}
                    expose: true
                MYSQL_PORT:
                    default: ${DATABASE_PORT}
                    expose: true
                MYSQL_ROOT_HOST:
                    default: '%'
                MYSQL_ROOT_PASSWORD:
                    default: ${MYSQL_ROOT_PASSWORD}
                    expose: true
                MYSQL_USER:
                    default: paperphone
                    expose: true
        - name: Redis
          icon: https://raw.githubusercontent.com/zeabur/service-icons/main/marketplace/redis.svg
          template: PREBUILT
          spec:
            id: redis
            source:
                image: redis:7-alpine
                command:
                    - redis-server
                    - --requirepass
                    - ${REDIS_PASS}
                    - --save
                    - "60"
                    - "1"
            ports:
                - id: database
                  port: 6379
                  type: TCP
            volumes:
                - id: data
                  dir: /data
            instructions:
                - title: Redis Connection String
                  content: redis://:${REDIS_PASS}@${PORT_FORWARDED_HOSTNAME}:${DATABASE_PORT_FORWARDED_PORT}
            env:
                REDIS_HOST:
                    default: ${CONTAINER_HOSTNAME}
                    expose: true
                REDIS_PASS:
                    default: ${REDIS_PASS}
                    expose: true
                REDIS_PORT:
                    default: ${DATABASE_PORT}
                    expose: true
                REDIS_URI:
                    default: redis://:${REDIS_PASS}@${REDIS_HOST}:${REDIS_PORT}
                    expose: true
        - name: LiveKit
          icon: https://raw.githubusercontent.com/livekit/livekit/master/favicon.svg
          dependencies:
            - Redis
          template: PREBUILT
          spec:
            id: livekit
            source:
                image: livekit/livekit-server:v1.9.6
            ports:
                - id: web
                  port: 7880
                  type: HTTP
                - id: rtc-tcp
                  port: 7881
                  type: TCP
            env:
                LIVEKIT_CONFIG:
                    default: |-
                        port: 7880
                        rtc:
                          tcp_port: 7881
                          udp_port: 7882
                          use_external_ip: true
                        redis:
                          address: ${REDIS_HOST}:${REDIS_PORT}
                          password: ${REDIS_PASS}
                        keys:
                          ${LIVEKIT_API_KEY}: ${LIVEKIT_API_SECRET}
          domainKey: LIVEKIT_DOMAIN
        - name: server
          icon: https://raw.githubusercontent.com/619dev/Paperphone-plus/main/client/public/icons/icon-512.png
          dependencies:
            - MySQL
            - Redis
            - LiveKit
          template: PREBUILT
          spec:
            id: server
            source:
                image: facilisvelox/paperphone-plus-server:latest
            ports:
                - id: web
                  port: 3000
                  type: HTTP
            volumes:
                - id: uploads
                  dir: /app/uploads
            env:
                ADMIN_PASSWORD:
                    default: ${ADMIN_PASSWORD}
                ADMIN_PATH:
                    default: ${ADMIN_PATH}
                APNS_BUNDLE_ID:
                    default: ${APNS_BUNDLE_ID}
                APNS_KEY_ID:
                    default: ${APNS_KEY_ID}
                APNS_PRIVATE_KEY:
                    default: ${APNS_PRIVATE_KEY}
                APNS_RELAY_KEY:
                    default: ${APNS_RELAY_KEY}
                APNS_RELAY_SECRET:
                    default: ${APNS_RELAY_SECRET}
                APNS_RELAY_URL:
                    default: ${APNS_RELAY_URL}
                APNS_SANDBOX:
                    default: ${APNS_SANDBOX}
                APNS_TEAM_ID:
                    default: ${APNS_TEAM_ID}
                DB_HOST:
                    default: ${MYSQL_HOST}
                DB_NAME:
                    default: ${MYSQL_DATABASE}
                DB_PASS:
                    default: ${MYSQL_PASSWORD}
                DB_PORT:
                    default: ${MYSQL_PORT}
                DB_USER:
                    default: paperphoneplus
                FCM_CLIENT_EMAIL:
                    default: ${FCM_CLIENT_EMAIL}
                FCM_PRIVATE_KEY:
                    default: ${FCM_PRIVATE_KEY}
                FCM_PROJECT_ID:
                    default: ${FCM_PROJECT_ID}
                FCM_RELAY_KEY:
                    default: ${FCM_RELAY_KEY}
                FCM_RELAY_SECRET:
                    default: ${FCM_RELAY_SECRET}
                FCM_RELAY_URL:
                    default: ${FCM_RELAY_URL}
                JWT_SECRET:
                    default: ${JWT_SECRET}
                LIVEKIT_API_KEY:
                    default: ${LIVEKIT_API_KEY}
                LIVEKIT_API_SECRET:
                    default: ${LIVEKIT_API_SECRET}
                LIVEKIT_URL:
                    default: wss://${LIVEKIT_DOMAIN}
                NTFY_BASE_URL:
                    default: ${NTFY_BASE_URL}
                NTFY_TOKEN:
                    default: ${NTFY_TOKEN}
                ONESIGNAL_APP_ID:
                    default: ${ONESIGNAL_APP_ID}
                ONESIGNAL_RELAY_KEY:
                    default: ${ONESIGNAL_RELAY_KEY}
                ONESIGNAL_RELAY_SECRET:
                    default: ${ONESIGNAL_RELAY_SECRET}
                ONESIGNAL_RELAY_URL:
                    default: ${ONESIGNAL_RELAY_URL}
                ONESIGNAL_REST_KEY:
                    default: ${ONESIGNAL_REST_KEY}
                PORT:
                    default: "3000"
                R2_ACCESS_KEY_ID:
                    default: ${R2_ACCESS_KEY_ID}
                R2_ACCOUNT_ID:
                    default: ${R2_ACCOUNT_ID}
                R2_BUCKET:
                    default: ${R2_BUCKET}
                R2_PUBLIC_URL:
                    default: ${R2_PUBLIC_URL}
                R2_SECRET_ACCESS_KEY:
                    default: ${R2_SECRET_ACCESS_KEY}
                REDIS_HOST:
                    default: ${REDIS_HOST}
                REDIS_PASS:
                    default: ${REDIS_PASS}
                REDIS_PORT:
                    default: ${REDIS_PORT}
                SERVER_URL:
                    default: ${ZEABUR_WEB_URL}
                    expose: true
                STICKER_PACKS:
                    default: ${STICKER_PACKS}
                TELEGRAM_BOT_TOKEN:
                    default: ${TELEGRAM_BOT_TOKEN}
                UPLOAD_DIR:
                    default: /app/uploads
                VAPID_PRIVATE_KEY:
                    default: ${VAPID_PRIVATE_KEY}
                VAPID_PUBLIC_KEY:
                    default: ${VAPID_PUBLIC_KEY}
                VAPID_SUBJECT:
                    default: ${VAPID_SUBJECT}
        - name: client
          icon: https://raw.githubusercontent.com/619dev/Paperphone-plus/main/client/public/icons/icon-512.png
          dependencies:
            - server
          template: PREBUILT
          spec:
            id: client
            source:
                image: facilisvelox/paperphone-plus-client:latest
            ports:
                - id: web
                  port: 80
                  type: HTTP
            env:
                SERVER_URL:
                    default: ${SERVER_URL}
          domainKey: PUBLIC_DOMAIN
localization:
    zh-CN:
        description: 微信风格的端对端加密即时通讯 App（React + Rust），支持无状态 ECDH 加密、群聊 Sender Key 端到端加密、 消息游标可靠同步与长期设备会话、LiveKit SFU 单聊与百人会议、实时变声、SOCKS5/HTTP/HTTPS 代理、 Cloudflare R2 文件存储、8 种语言与 iOS PWA 部署，完全可自建。
        readme: "# PaperPhonePlus\n\n**微信风格**的端对端加密即时通讯应用。\n使用 **React 19 + TypeScript + Vite** 前端和 **Rust (Axum)** 后端重写，\n获得极致性能和类型安全。\n\n## 安全模型\n- **无状态 ECDH + XSalsa20-Poly1305** — 逐消息临时密钥，前向保密无需会话状态同步\n- **零知识服务器** — 仅存储密文，私钥仅在设备本地，永不上传\n- **四层密钥持久化** — 内存 → localStorage → sessionStorage → IndexedDB，Android WebView 重启后仍可恢复\n- **发送缓存仅保留密文（v2.3.5）** — 私聊消息在进入持久化离线缓存前完成加密，等待服务器确认期间不会短暂落盘明文\n\n## 功能亮点\n- \U0001F4F9 **视频/语音通话** — 1:1 私聊和群会议统一使用 LiveKit SFU（最多 100 人，支持主席全员静音和讲课模式）\n- \U0001F399️ **变声功能** — 语音消息 / 1v1 通话 / 群组通话均支持实时变声，3 档可选（0.8x 低沉 / 1.0x 正常 / 1.2x 尖锐），基于 Web Audio API 音频处理链\n- ⏱️ **安全语音录制** — 最长 120 秒自动停止，录音期间保持屏幕唤醒，离开页面时可靠释放麦克风\n- \U0001F4F1 **会话保持** — 30 分钟访问令牌与 90 天设备刷新会话，VPN/IP/网络变化时自动重连且无需输入密码\n- \U0001F4E8 **可靠消息同步** — 双向心跳、持久化发件箱、消息幂等 ID 与服务端序号游标补偿\n- \U0001F4F4 **离线访问** — 按账户隔离缓存联系人、群组、聊天记录、朋友圈、时间线和媒体；离线发送自动重试\n- \U0001F50E **Unicode 好友搜索** — 支持中文输入法组合状态、NFC 归一化和中文用户名/昵称搜索\n- \U0001F465 **群聊** — 最多 2000 人群组，支持「加密」与「未加密」两种模式（群主可切换，切换清空历史消息）。加密模式采用 Signal 风格 Sender Key 协议（XSalsa20-Poly1305 对称加密 + ECDH 密钥分发），仅群成员可解密消息；加密模式下无法使用群机器人。免打扰模式，成员管理\n- \U0001F310 **朋友圈** — 文字 + 最多 9 张图或 1 个视频（≤10 分钟），点赞、评论、标签可见性控制\n- \U0001F464 **个人资料** — 联系人资料页（朋友圈动态 +「不看此人朋友圈」与「不让他看我的朋友圈」双向隐私控制）\n- \U0001F4F0 **时间线** — 小红书风格公开发帖区，双列瀑布流布局，最多50个图片/视频+2000字文字，支持匿名发帖、点赞、评论\n- ⏱️ **消息自动删除** — 5 档可选（永不/1天/3天/1周/1月）\n- \U0001F514 **消息推送** — Web Push (VAPID) + FCM + OneSignal + ntfy + APNS 五通道（iOS 原生 + 国产安卓免 Google 服务）\n- \U0001F310 **多语言** — 中文、英文、日语、韩语、法语、德语、俄语、西班牙语\n- \U0001F4F1 **iOS 永久免签** — Safari「添加到主屏幕」，无需企业证书\n- \U0001F4AC 富文本消息、图片、视频、文档文件（PDF/DOCX/XLSX 等带类型图标）、语音、Emoji（200+，8 分类）、Telegram 贴纸包、送达回执、打字状态\n- \U0001F5C2️ **Cloudflare R2** 对象存储（图片、语音与视频）\n- \U0001F3F7️ **好友标签** — 标签分类筛选通讯录 + 朋友圈可见性控制\n- \U0001F3AD **Telegram 贴纸包** — 动态贴纸包管理，可自定义包列表（内置 13 个默认包），数量无上限\n- \U0001F511 **两步验证 (2FA)** — Google Authenticator TOTP，8 个恢复码\n- \U0001F4F7 **扫码加好友/入群** — 扫一扫二维码添加好友、加入群聊，群二维码可设置有效期（1周/1月/3月）\n- \U0001F46B **好友系统** — 添加好友需对方审核，支持 512 字验证消息；备注名称；好友标签分组\n- \U0001F4E4 **文件上传** — 单文件最大 500MB，支持 Cloudflare R2 或本地存储，带进度条动画\n- \U0001F310 **代理设置** — 支持 SOCKS5 / HTTP / HTTPS 代理协议，登录页和设置页均可配置（地址、端口、用户名、密码）\n- \U0001F6E1️ **内容审核** — 用户举报（6 类原因）+ 拉黑用户（即时屏蔽动态/消息）+ 使用条款 EULA\n- \U0001F527 **管理后台** — 内嵌 Web 管理面板（`/admin`，路径可通过 `ADMIN_PATH` 自定义），密码保护，审核举报、删除内容、封禁用户，支持 8 种语言\n\n## 部署后操作\n1. 打开分配给 **client** 服务的域名。\n2. 注册账号 — 密钥在本地设备生成。\n3. （可选）在 **server** 服务设置 `R2_*` 变量，启用图片/语音/视频上传功能。\n4. （可选）设置 `VAPID_*` 变量启用浏览器推送通知，`FCM_*` 启用原生 Android 推送（Capacitor），`APNS_*` 启用 iOS 原生推送。\n5. （可选）设置 `TELEGRAM_BOT_TOKEN` 和 `STICKER_PACKS` 启用 Telegram 贴纸包功能。\n6. （可选）访问 `/admin`（或自定义 `ADMIN_PATH`）进入管理后台审核举报。生产环境请设置 `ADMIN_PASSWORD`。\n7. 升级后先检查 **server** 日志并确认健康检查通过，再更新客户端；切换一次 VPN/网络并进行一次断网发送，验证自动恢复且消息只入库一次。\n8. v2.3.5 升级后可在短暂离线时发送一条私聊消息并确认重连后自动重试；Web 客户端对待发送消息只持久化密文。\n\nMySQL 已使用 `utf8mb4` 和 `utf8mb4_unicode_ci` 启动；server 启动时会自动迁移并验证消息序号、幂等 ID、刷新会话及搜索字符集字段。**升级时必须先重新部署 server，等待健康检查通过后再更新客户端；生产升级前请备份 MySQL。**\n\n## Zeabur LiveKit 通话说明\n本模板会部署 LiveKit 的 WebSocket/API（7880）和 ICE/TCP 回退端口（7881）。\nZeabur 当前不支持对外暴露 UDP 服务端口，因此私聊和群会议可以通过 TCP 连接，但弱网下延迟和画质可能不如 UDP。\n模板已保留 UDP 7882 配置，待 Zeabur 支持 UDP 后即可开放。当前如需生产级百人会议，建议使用\nLiveKit Cloud，或把 LiveKit 部署到可开放 UDP 7882 的云主机，再修改 server 的 `LIVEKIT_URL`。\n\n## 进阶方案：Zeabur + Vercel 混合部署\n部署完成后，可以手动删除 Zeabur 上的 **client** 服务，改用 **Vercel** 部署前端。\n这样 server/MySQL/Redis 由 Zeabur 托管，前端由 Vercel CDN 加速，全球访问更快。\nVercel 中设置环境变量 `VITE_API_URL` 指向 Zeabur 上 server 服务的公网域名即可。"
