Files
PaperPhone/README.md
2026-04-01 10:27:52 +00:00

17 KiB
Raw Blame History

PaperPhone IM

🌐 其他语言 / Other Languages: English · 日本語 · 한국어 · Français · Deutsch · Русский · Español

一款微信风格的端对端加密即时通讯应用,采用无状态 ECDH + XSalsa20-Poly1305 逐消息加密,支持 iOS PWA 永久免签与 Cloudflare R2 文件存储。

Node.js MySQL Redis WebRTC

Deploy on Zeabur


ui1 ui2 ui3 ui4 ui5 ui6 ui7 ui8 ui9 ui10 ui11 ui12 ui13 ui14 ## 特性
功能 说明
🔐 端对端加密 无状态 ECDH + XSalsa20-Poly1305逐消息临时密钥前向保密
🗝️ 零知识服务器 服务器只存储密文,私钥仅在设备本地(四层持久化)
📹 视频/语音通话 WebRTC P2P1:1+ Mesh多人Cloudflare TURN 穿透
👥 群聊 最多 2000 人群组,纯文本消息(无加密),免打扰模式,成员管理
⏱️ 消息自动删除 5 档可选(永不/1天/3天/1周/1月私聊双方均可设置群聊群主专属
🔔 消息推送 Web Push (VAPID) + OneSignal 双通道,离线也能收到通知
🌐 多语言 中文、英文、日语、韩语、法语、德语、俄语、西班牙语(自动检测 + 手动切换)
📱 iOS 永久免签 PWA H5 → Safari「添加到主屏幕」无需企业证书
💬 消息功能 文字、图片、视频、文档文件PDF/DOCX/XLSX 等带类型图标、语音消息、Emoji 面板200+8 分类、Telegram 贴纸包、已读状态
🌐 朋友圈 发动态(文字+最多9张图或1个视频≤10分钟、点赞显示好友头像、评论、标签可见性控制
👤 个人资料 联系人资料页(头像/昵称/朋友圈动态),支持「不看此人朋友圈」与「不让他看我的朋友圈」双向隐私控制
📰 时间线 小红书风格公开发帖区——双列瀑布流布局,图片/视频+文字最多50个媒体、2000字支持匿名发帖、点赞、评论
🏷️ 好友标签 为好友设置多个标签12色预设调色板按标签分类筛选通讯录
🗂️ R2 对象存储 Cloudflare R2 存储图片/语音,可选公开 CDN 直链
🔑 两步验证 (2FA) Google Authenticator 兼容 TOTP 验证8 个一次性恢复码,登录时强制验证
🏗️ 可自托管 Docker Compose 一键部署,支持 Node.js + Redis 多节点

技术栈

后端 (server/)
  Node.js 20 + Express + ws
  MySQL 8.0  — 用户/消息持久化
  Redis      — 在线状态 + 跨节点路由
  Cloudflare R2 — 图片/语音文件存储S3 兼容 API
  JWT + bcrypt 认证

前端 (client/)
  原生 HTML + Vanilla JS (ESM无打包工具)
  libsodium-wrappers (WebAssembly, Curve25519 / XSalsa20-Poly1305)
  WebRTC API  — 视频/语音通话
  PWA: manifest.json + Service Worker

加密层
  无状态 ECDH + XSalsa20-Poly1305 — 逐消息临时 ECDH 密钥对,前向保密
  私钥四层持久化: 内存 → localStorage → sessionStorage → IndexedDB
  私钥全程存储在设备本地,从不上传服务器

快速启动

方式零Zeabur 一键云部署

Deploy on Zeabur

Note

部署完成后需手动完成一步配置,否则注册/登录无法使用:

  1. 进入 Zeabur 控制台 → server 服务 → Environment Variables → 复制 ZEABUR_WEB_URL 的值(如 http://10.43.x.x:3000
  2. 进入 client 服务 → Environment Variables → 添加变量 SERVER_URL = 上一步复制的值
  3. Restart client 服务

已知注意事项:

  • 首次启动 server 会自动创建数据库表(CREATE TABLE IF NOT EXISTS),无需手动导入 schema
  • Redis 在集群内无需密码,已默认关闭认证
  • 若需配置 MySQL root 密码,可在 server 服务的 DB_PASS 里手动填写 MySQL 服务的 MYSQL_ROOT_PASSWORD
  • 如需获取某个服务容器的内网 IP,在 Zeabur 控制台打开该服务的命令行Terminal输入
    hostname -i
    

方式一Docker Compose推荐无需本地构建

# 克隆仓库
git clone <repo-url> && cd paperphone

# 复制并编辑环境变量
cp server/.env.example server/.env
# 按需编辑DB_PASS / JWT_SECRET / CF_CALLS_APP_ID 等

# 拉取镜像并一键启动
docker compose up -d

# 查看服务状态
docker compose ps

# 访问
open http://localhost

镜像已发布至 Docker Hub

  • facilisvelox/paperphone-client:latest
  • facilisvelox/paperphone-server:latest

注意server 首次启动会自动初始化数据库 schema无需手动导入 SQL 文件。

方式二:本地手动启动

1. 准备环境

# 复制并编辑环境变量
cp server/.env.example server/.env
# 填写 DB_HOST / DB_PASS / REDIS_HOST / R2_* 等

# 注server 首次启动会自动执行 schema.sql无需手动导入

2. 启动后端

cd server
npm install
npm run dev   # → http://localhost:3000

3. 启动前端

npx serve client -p 8080
# → http://localhost:8080

视频通话配置

视频通话使用 WebRTC P2P局域网内开箱即用。跨网络通话需要配置 TURN 服务器(用于 NAT 穿透)。

使用 Cloudflare TURN推荐

  1. 登录 Cloudflare DashboardWorkers & PagesCalls → 创建 App
  2. 获取 App IDApp SecretToken Key
  3. 填入 server/.env
CF_CALLS_APP_ID=your_app_id_here
CF_CALLS_APP_SECRET=your_app_secret_here
  1. 重启后端TURN 凭据会在每次通话时自动刷新TTL 86400s

未配置时:自动降级为 STUN onlyGoogle + Cloudflare 公共 STUN局域网内可正常通话。

通话功能说明

类型 技术方案 适用场景
私聊 1:1 视频 WebRTC P2P + TURN 所有场景
私聊 1:1 语音 WebRTC P2P + TURN 所有场景
群组多人语音/视频 WebRTC Mesh全连接 ≤ 6 人

消息推送配置

离线消息通知通过双通道推送,最大化消息送达率:

通道 适用场景 配置
Web Push (VAPID) 浏览器 (Chrome/Edge/Firefox) + iOS PWA (Safari 16.4+) VAPID 密钥
OneSignal Median.co 打包的原生 Android/iOS App OneSignal App ID + REST Key

配置 Web Push

  1. 生成 VAPID 密钥(仅需一次):
cd server
npx web-push generate-vapid-keys
  1. 填入 server/.env
VAPID_PUBLIC_KEY=your_public_key_here
VAPID_PRIVATE_KEY=your_private_key_here
VAPID_SUBJECT=mailto:admin@your-domain.com
  1. 重启服务器,用户可在设置页开启通知

iOS 用户需先将应用「添加到主屏幕」,且仅 iOS 16.4+ 支持。

配置 OneSignalMedian.co 原生 App

  1. OneSignal Dashboard 创建 App 并配置 Firebase
  2. 在 Median.co 中启用 OneSignal 并填入 App ID
  3. 将 OneSignal 的 App IDREST API Key 填入 server/.env
ONESIGNAL_APP_ID=your_onesignal_app_id
ONESIGNAL_REST_KEY=your_onesignal_rest_api_key

未配置时:推送功能静默禁用,不影响其他功能。


iOS 永久免签部署

  1. 部署到有 HTTPS 域名的服务器WebRTC 和加密 API 需要 HTTPS
  2. Safari 打开 https://your.domain.com
  3. 点击底部分享按钮 ⬆️
  4. 选择「添加到主屏幕」→「添加」

即可获得与原生 App 相同的体验,无需 Apple 企业证书,永久有效!


生产部署Nginx

server {
    listen 443 ssl http2;
    server_name your.domain.com;

    ssl_certificate     /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    # 前端静态文件
    location / {
        root /path/to/paperphone/client;
        try_files $uri /index.html;
    }

    # API
    location /api/ {
        proxy_pass http://localhost:3000;
        proxy_set_header Host $host;
    }

    # WebSocket 信令
    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;
    }
}

项目结构

paperphone/
├── docker-compose.yml
├── server/
│   ├── .env                    # 环境变量(如 Cloudflare TURN 密钥)
│   └── src/
│       ├── app.js              # Express 应用入口
│       ├── routes/
│       │   ├── auth.js         # 注册/登录(含 X3DH 公钥上传)
│       │   ├── users.js        # 用户搜索 / Prekey 下载
│       │   ├── friends.js      # 好友申请 / 接受(含离线推送)
│       │   ├── groups.js       # 群组管理
│       │   ├── messages.js     # 历史消息(密文分页)
│       │   ├── upload.js       # Cloudflare R2 文件上传
│       │   ├── files.js        # 文件代理R2_PUBLIC_URL 未设时)
│       │   ├── moments.js      # 朋友圈(动态/点赞/评论/用户级隐私)
│       │   ├── timeline.js     # 时间线(公开发帖/点赞/评论/匿名)
│       │   ├── calls.js        # TURN 凭据派发
│       │   ├── push.js         # 推送订阅管理Web Push + OneSignal
│       │   ├── stickers.js     # Telegram 贴纸包代理(缓存)
│       │   └── totp.js         # TOTP 两步验证(设置/验证/恢复码)
│       ├── services/
│       │   ├── push.js         # Web Push VAPID 服务
│       │   └── onesignal.js    # OneSignal REST API 服务
│       └── ws/
│           └── wsServer.js     # WebSocket 路由(含通话信令 + 离线推送)
│
└── client/
    ├── index.html              # SPA 入口 + PWA 元数据 + Median 推送桥接
    ├── manifest.json           # PWA 清单
    ├── sw.js                   # Service Worker离线缓存 + 推送通知)
    └── src/
        ├── style.css           # Premium 设计系统(暗色/亮色,玻璃拟态)
        ├── app.js              # 路由 + 全局状态 + 来电监听
        ├── api.js              # HTTP 客户端
        ├── socket.js           # WebSocket 客户端(自动重连)
        ├── i18n.js             # 多语言引擎zh/en/ja/ko/fr/de/ru/es
        ├── services/
        │   ├── webrtc.js       # WebRTC 管理器CallManager
        │   └── pushNotification.js  # 推送订阅管理Web Push + Median 桥接)
        ├── crypto/
        │   ├── ratchet.js      # X3DH + Double Ratchet + ML-KEM-768
        │   └── keystore.js     # 四层私钥持久化(内存/localStorage/sessionStorage/IndexedDB
        ├── pages/
        │   ├── login.js        # 登录/注册(含密钥生成、语言切换)
        │   ├── chats.js        # 会话列表
        │   ├── chat.js         # 聊天窗口E2E 加密、通话按钮)
        │   ├── groups.js       # 群聊列表(创建群、搜索群)
        │   ├── groupInfo.js    # 群信息(成员管理、免打扰、退出/解散)
        │   ├── contacts.js     # 通讯录(好友申请/在线状态)
        │   ├── discover.js     # 发现页
        │   ├── profile.js      # 我的/设置语言、指纹、通知、PWA
        │   ├── userProfile.js   # 联系人资料页(朋友圈动态 + 隐私开关)
        │   └── call.js         # 通话 UI来电/通话中/多人视频)
        └── components/
            ├── tagManager.js   # 标签管理组件
            └── momentCard.js   # 朋友圈卡片组件(可复用)
        └── pages/
            ...
            ├── timeline.js     # 时间线(小红书风格瀑布流 + 发帖 + 详情)

数据库结构

共 18 张表,首次启动自动创建(CREATE TABLE IF NOT EXISTS

表名 说明
users 用户信息 + ECDH/OPK 公钥
prekeys X3DH 一次性预密钥池
friends 好友关系pending/accepted/blocked
groups / group_members 群组 + 成员(含免打扰状态)
messages 加密消息(离线缓冲,送达后可删)
moments 朋友圈动态(文字 ≤1024 字)
moment_images 动态图片(每条最多 9 张)
moment_videos 动态视频(封面图+时长,每条最多 1 个≤10 分钟)
moment_likes 点赞(每用户每条唯一)
moment_comments 评论(最多 512 字/条)
push_subscriptions Web Push 推送订阅VAPID
onesignal_players OneSignal 设备注册Median.co
user_totp TOTP 两步验证密钥与恢复码
moment_privacy 朋友圈用户级隐私设置(不看/不让看)
timeline_posts 时间线帖子(文字 ≤2000 字,支持匿名)
timeline_media 时间线媒体(图片/视频,每帖最多 50 个)
timeline_likes 时间线点赞
timeline_comments 时间线评论(支持匿名)

安全模型

注册时:
  设备本地生成 IK身份密钥+ SPK签名预密钥+ 10x OPK一次性预密钥
  公钥上传服务器,私钥四层持久化,永不离开设备

发送消息时:
  发送方下载接收方 IK 公钥
  生成临时 ECDH 密钥对(每条消息独立)
  X25519 ECDH → 共享秘密 → XSalsa20-Poly1305 加密
  临时公钥附在消息 header 中,接收方解密后销毁

服务器所见:
  ✅ 密文 blob + 路由元数据(发件人/收件人 UUID
  ❌ 明文 / 私钥 / 临时密钥 / 通话内容

环境变量参考

变量 说明 默认值
PORT 服务端口 3000
JWT_SECRET JWT 签名密钥(生产必改 dev_secret
DB_HOST / DB_PASS / DB_NAME MySQL 连接配置
REDIS_HOST / REDIS_PASS Redis 连接配置
R2_ACCOUNT_ID Cloudflare 账号 ID
R2_ACCESS_KEY_ID R2 API Token 的 Access Key
R2_SECRET_ACCESS_KEY R2 API Token 的 Secret Key
R2_BUCKET R2 Bucket 名称
R2_PUBLIC_URL R2 公开 URL可选设置后文件走 CDN 直链
CF_CALLS_APP_ID Cloudflare Calls App ID可选
CF_CALLS_APP_SECRET Cloudflare Calls App Secret可选
VAPID_PUBLIC_KEY Web Push VAPID 公钥(可选)
VAPID_PRIVATE_KEY Web Push VAPID 私钥(可选)
VAPID_SUBJECT VAPID 联系邮箱(可选) mailto:admin@paperphone.app
ONESIGNAL_APP_ID OneSignal App ID可选Median.co
ONESIGNAL_REST_KEY OneSignal REST API Key可选
TELEGRAM_BOT_TOKEN Telegram Bot Token可选贴纸包代理
STICKER_PACKS 自定义贴纸包列表(可选,逗号分隔 包名:显示名,数量无上限) 内置 8 个默认包

如果这个项目对你有用的话,请我喝罐可乐吧。 qrcode