169 lines
8.3 KiB
Markdown
169 lines
8.3 KiB
Markdown
# Toot Worker
|
|
|
|
一个运行在 Cloudflare Workers 上的单用户联邦宇宙发布软件,使用:
|
|
|
|
- Workers: HTTP API、ActivityPub 路由、Mastodon API 兼容层
|
|
- D1: 用户、OAuth 应用/Token、嘟文、媒体索引、关注、收藏、转发、通知、提及、话题标签、收藏夹、置顶、远端 actor 与远端嘟文缓存
|
|
- R2: 媒体文件
|
|
- KV: OAuth access token 会话(D1 同步保留,便于管理)
|
|
|
|
当前目标:让常见 Mastodon App 完成实例发现、创建 OAuth App、登录、上传媒体、发布嘟文、读取公开/家庭/话题时间线、收藏/转发/回复/收藏夹/置顶、查看通知、搜索、关注/取关远端账号,同时支持单用户实例与 Fediverse 双向联邦。
|
|
|
|
## 本地运行
|
|
|
|
```bash
|
|
npm install
|
|
npm run db:local
|
|
npm run dev
|
|
```
|
|
|
|
默认管理员账号来自 `wrangler.jsonc`:
|
|
|
|
- 用户名:`admin`
|
|
- 密码:`change-me-before-deploy`
|
|
|
|
部署前必须改掉 `ADMIN_PASSWORD`,更推荐用 Cloudflare secret 管理密码:
|
|
|
|
```bash
|
|
wrangler secret put ADMIN_PASSWORD
|
|
```
|
|
|
|
`PUBLIC_BASE_URL` 必须在首次正式部署前改成你的稳定实例域名,例如:
|
|
|
|
```json
|
|
"PUBLIC_BASE_URL": "https://social.example.com"
|
|
```
|
|
|
|
这个值会进入:
|
|
|
|
- Actor ID
|
|
- Object ID
|
|
- WebFinger 返回值
|
|
- 媒体和头像 URL
|
|
|
|
一旦开始对外联邦后,不要再改域名,否则远端会把你视为另一个实例身份。
|
|
|
|
### 媒体 CDN 加速(可选但推荐)
|
|
|
|
默认情况下,客户端拉取上传媒体走 `${PUBLIC_BASE_URL}/media/<key>`,会经过 Worker 反代 R2,消耗 Worker 请求数和 CPU。生产建议:
|
|
|
|
1. 在 Cloudflare R2 控制台给 `toot-media` 绑定一个 custom domain(如 `media.social.example.com`),开启公开访问 + CDN。
|
|
2. 把该域名填进 `wrangler.jsonc` 的 `MEDIA_BASE_URL`,例如 `"MEDIA_BASE_URL": "https://media.social.example.com"`。
|
|
|
|
设置后,Mastodon 客户端拿到的 `media_attachments.url` 会直接指向 CDN 域名,不再经过 Worker。Worker 自己的 `/media/<key>` 路径仍然保留,可以作为后备访问入口。
|
|
|
|
## Cloudflare 资源
|
|
|
|
```bash
|
|
wrangler d1 create toot_db
|
|
wrangler r2 bucket create toot-media
|
|
wrangler kv namespace create KV
|
|
```
|
|
|
|
把返回的 ID 填回 `wrangler.jsonc`,然后应用迁移并部署:
|
|
|
|
```bash
|
|
npm run db:remote
|
|
npm run deploy
|
|
```
|
|
|
|
## 已实现接口
|
|
|
|
### Mastodon API 兼容
|
|
|
|
实例 / 应用 / 鉴权:
|
|
|
|
- `GET /api/v1/instance`、`GET /api/v2/instance`
|
|
- `POST /api/v1/apps`、`GET /api/v1/apps/verify_credentials`
|
|
- `GET /oauth/authorize`、`POST /oauth/authorize`
|
|
- `POST /oauth/token`、`POST /oauth/revoke`
|
|
|
|
账号:
|
|
|
|
- `GET /api/v1/accounts/verify_credentials`
|
|
- `PATCH /api/v1/accounts/update_credentials`(自动联邦 Update Person)
|
|
- `GET /api/v1/accounts/relationships`
|
|
- `GET /api/v1/accounts/:id`
|
|
- `GET /api/v1/accounts/:id/statuses`
|
|
- `POST /api/v1/accounts/:id/follow`、`POST /api/v1/accounts/:id/unfollow`(向远端发送 Follow / Undo)
|
|
- `GET /api/v1/follow_requests`、`POST /api/v1/follow_requests/:id/authorize`、`/reject`(stub,默认全自动接受)
|
|
|
|
嘟文:
|
|
|
|
- `POST /api/v1/statuses`(支持 `media_ids`、`spoiler_text`、`sensitive`、`in_reply_to_id`、`visibility`、`language`,自动解析 `@user`/`@user@host` 提及和 `#hashtag`,投递 Create 给 followers 与 mention)
|
|
- `GET /api/v1/statuses/:id`、`DELETE /api/v1/statuses/:id`(联邦 Delete 出站)
|
|
- `GET /api/v1/statuses/:id/context`
|
|
- `POST /api/v1/statuses/:id/favourite`、`/unfavourite`(联邦 Like / Undo Like)
|
|
- `POST /api/v1/statuses/:id/reblog`、`/unreblog`(联邦 Announce / Undo Announce)
|
|
- `POST /api/v1/statuses/:id/bookmark`、`/unbookmark`、`/pin`、`/unpin`(本地落库)
|
|
- `GET /api/v1/bookmarks`、`GET /api/v1/favourites`(列出本地 bookmark / favourite)
|
|
|
|
时间线 / 通知 / 媒体 / 搜索 / 其它:
|
|
|
|
- `GET /api/v1/timelines/public`(分页支持 `max_id` / `since_id` / `min_id`,响应携带 `Link` 头)
|
|
- `GET /api/v1/timelines/home`(合并本地嘟文 + 关注的远端账号缓存嘟文,按时间排序)
|
|
- `GET /api/v1/timelines/tag/:tag`、`GET /api/v1/tags/:name`(话题时间线 + 话题元数据)
|
|
- `GET /api/v1/notifications`、`POST /api/v1/notifications/clear`、`POST /api/v1/notifications/:id/dismiss`
|
|
- `POST /api/v1/media`、`POST /api/v2/media`、`PUT /api/v1/media/:id`
|
|
- `GET /api/v2/search`、`GET /api/v1/search`(本地账号 / 嘟文 / 话题标签 + 跨站 WebFinger 解析 `acct:` 查询)
|
|
- `GET /api/v1/custom_emojis`、`GET /api/v1/filters`、`GET /api/v1/trends/tags`、`GET /api/v1/markers`(stub)
|
|
- `POST /api/v1/push/subscription`(返回 422,目前不支持推送)
|
|
|
|
### ActivityPub / 发现
|
|
|
|
- `GET /.well-known/webfinger?resource=acct:user@example.com`
|
|
- `GET /.well-known/nodeinfo`、`GET /.well-known/host-meta`
|
|
- `GET /nodeinfo/2.0`
|
|
- `GET /users/:username`(含 `endpoints.sharedInbox`、`icon`、`image`、`publicKey`)
|
|
- `GET /users/:username/followers`、`/following`
|
|
- `GET /users/:username/outbox`(支持 `?page=true` 翻页)
|
|
- `POST /users/:username/inbox`、`POST /inbox`(共享 inbox)
|
|
- `GET /objects/:id`(嘟文已删除时返回 `Tombstone`,HTTP 410)
|
|
|
|
入站 inbox 处理类型:`Follow` / `Undo(Follow)` / `Accept(Follow)` / `Reject(Follow)` / `Like` / `Undo(Like)` / `Announce` / `Undo(Announce)` / `Delete(Note)`(同时清远端嘟文缓存)/ `Delete(Person)`(同时清缓存与关注关系)/ `Update(Person)` / `Update(Note)` / `Create(Note)`(被关注的远端账号公开嘟文会写入 `cached_statuses` 给 home timeline 使用,同时若 mention/回复本地用户会触发通知)。
|
|
|
|
## 安全
|
|
|
|
- 密码哈希使用 **PBKDF2-SHA256 / 100000 iterations**,带每用户随机 16 字节 salt,旧版 `salt.hash` 哈希也能继续验证(便于无缝升级)。
|
|
- HTTP Signature(rsa-sha256)出站:签名 `(request-target)`、`host`、`date`、`digest`,自动计算 SHA-256 digest。
|
|
- HTTP Signature 入站验证:
|
|
- 强制要求 `Date` 头,允许 ±12 小时时钟偏移
|
|
- `POST` 必须带 `Digest`,且与 body 哈希一致
|
|
- 通过签名头里的 `keyId` 取得远端 actor 公钥(并在 `actor_cache` 表中缓存),拒绝 `body.actor` 与 actor cache 不一致的请求
|
|
- 验证 `host` 头匹配请求 URL
|
|
- 远端 actor 公钥与 inbox 写入 `actor_cache`,默认 24 小时 TTL,远端 `Delete(Person)` 会清理缓存与所有相关关注 / 收藏 / 转发 / 通知。
|
|
- 重放保护:已处理过的 `activity.id` 会写入 `remote_activities`,重复投递被静默丢弃(返回 202)。
|
|
|
|
## 数据库结构
|
|
|
|
迁移文件:
|
|
|
|
- `migrations/0001_initial.sql` — 基础表(users / oauth_apps / oauth_codes / statuses / media / follows / remote_activities)
|
|
- `migrations/0002_features.sql` — 通知 / 收藏 / 转发 / 提及 / 话题标签 / actor 缓存 / 出站关注 / 删除墓碑 / 嘟文扩展字段(summary / sensitive / language)
|
|
- `migrations/0003_bookmarks_cache.sql` — 收藏夹(bookmarks)/ 置顶(pinned_statuses)/ 远端嘟文缓存(cached_statuses)/ OAuth Token 持久表
|
|
|
|
## 重要限制
|
|
|
|
这是一个单用户可运行实现,不是完整 Mastodon 服务端:
|
|
|
|
- 只支持单管理员账号自动初始化,不开放注册
|
|
- 本地状态的可见性已做基础控制:
|
|
- `public` / `unlisted` 可公开读取; ActivityPub outbox 只暴露这两类状态
|
|
- `private` 会按 followers-only 投递,本地读取限作者和本地关注者
|
|
- `direct` 仍没有完整受众表,本地读取保守限制为作者可见,不应当作为完整私信系统使用
|
|
- 远端嘟文缓存只在被本地账号关注的 actor 发出的入站 `Create(Note)` 时写入,不抓取历史 outbox
|
|
- 远端缓存嘟文只保留正文、CW、语言和附件; `mentions`、`tags`、互动计数等不会完整恢复,在 home timeline 中统一按 `visibility: public` 返回
|
|
- 媒体上传只支持 `image/jpeg`、`image/png`、`image/gif`、`image/webp`,单文件 10MB,单条状态的附件数量不做服务端限制; 头像和封面同样只按图片路径处理
|
|
- 没有实现接口级限流、反滥用或审核流; `follow_requests` 相关接口仍是 stub
|
|
- 没有实现轮询(poll)、列表(list)、推送(push)、未来嘟文(scheduled)等
|
|
|
|
## 参考
|
|
|
|
- Cloudflare Workers 绑定:https://developers.cloudflare.com/workers/runtime-apis/bindings/
|
|
- Cloudflare D1:https://developers.cloudflare.com/d1/
|
|
- Cloudflare R2:https://developers.cloudflare.com/r2/
|
|
- Cloudflare KV:https://developers.cloudflare.com/kv/
|
|
- Mastodon API:https://docs.joinmastodon.org/methods/
|
|
- ActivityPub:https://www.w3.org/TR/activitypub/
|
|
- HTTP Signatures:https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12
|