Files
2026-05-16 10:13:22 +08:00

9.9 KiB

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 双向联邦。

本地运行

npm install
npm run db:local
npm run dev

默认管理员用户名来自 wrangler.jsoncADMIN_USERNAME,当前示例值为 sun。密码必须通过 Secret 或本地开发变量提供:

生产部署:

wrangler secret put ADMIN_PASSWORD

本地开发可以创建 .dev.vars:

ADMIN_PASSWORD=change-me-before-deploy

PUBLIC_BASE_URL 必须在首次正式部署前改成你的稳定实例域名,例如:

"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.jsoncMEDIA_BASE_URL,例如 "MEDIA_BASE_URL": "https://media.social.example.com"

设置后,Mastodon 客户端拿到的 media_attachments.url 会直接指向 CDN 域名,不再经过 Worker。Worker 自己的 /media/<key> 路径仍然保留,可以作为后备访问入口。

Cloudflare 资源

wrangler d1 create toot_db
wrangler r2 bucket create toot-media
wrangler kv namespace create KV

把返回的 ID 填回 wrangler.jsonc,然后应用迁移并部署:

npm run db:remote
npm run deploy

已实现接口

Mastodon API 兼容

实例 / 应用 / 鉴权:

  • GET /api/v1/instanceGET /api/v2/instance
  • POST /api/v1/appsGET /api/v1/apps/verify_credentials
  • GET /oauth/authorizePOST /oauth/authorize
  • POST /oauth/tokenPOST /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/followPOST /api/v1/accounts/:id/unfollow(向远端发送 Follow / Undo)
  • GET /api/v1/follow_requestsPOST /api/v1/follow_requests/:id/authorize/reject(stub,默认全自动接受)

嘟文:

  • POST /api/v1/statuses(支持 media_idsspoiler_textsensitivein_reply_to_idvisibilitylanguagepoll[...]scheduled_at,自动解析 @user/@user@host 提及和 #hashtag,投递 Create 给 followers 与 mention)
  • GET /api/v1/statuses/:idGET /api/v1/statuses/:id/sourcePUT / PATCH /api/v1/statuses/:id(联邦 Update Note 出站)、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/polls/:idPOST /api/v1/polls/:id/votes
  • GET /api/v1/scheduled_statusesGET / PUT / DELETE /api/v1/scheduled_statuses/:id
  • GET /api/v1/bookmarksGET /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/list/:id
  • GET /api/v1/timelines/tag/:tagGET /api/v1/tags/:name(话题时间线 + 话题元数据)
  • GET / POST /api/v1/listsGET / PUT / DELETE /api/v1/lists/:idGET / POST / DELETE /api/v1/lists/:id/accounts
  • GET /api/v1/notificationsPOST /api/v1/notifications/clearPOST /api/v1/notifications/:id/dismiss
  • POST /api/v1/mediaPOST /api/v2/mediaPUT /api/v1/media/:id
  • GET /api/v2/searchGET /api/v1/search(本地账号 / 嘟文 / 话题标签 + 跨站 WebFinger 解析 acct: 查询 + 粘贴远端嘟文 URL 按需抓取缓存)
  • GET /api/v1/custom_emojisGET /api/v1/filtersGET /api/v1/trends/tagsGET /api/v1/markersPOST /api/v1/markers
  • GET / POST / PUT / DELETE /api/v1/push/subscription(存储 Web Push 订阅参数;实际推送投递仍需 VAPID/加密发送实现)

ActivityPub / 发现

  • GET /.well-known/webfinger?resource=acct:user@example.com
  • GET /.well-known/nodeinfoGET /.well-known/host-meta
  • GET /nodeinfo/2.0
  • GET /users/:username(含 endpoints.sharedInboxiconimagepublicKey)
  • GET /users/:username/followers/following
  • GET /users/:username/outbox(支持 ?page=true 翻页)
  • POST /users/:username/inboxPOST /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)(被关注的远端账号公开 / followers-only Note,或投递给本地用户的 Note 会写入 cached_statuses 给 home timeline 和通知使用,同时若 mention/回复本地用户会触发通知)。

出站 ActivityPub 投递会先写入 D1 outgoing_deliveries 队列,再由请求后的 waitUntil 和每分钟 Cron 触发器后台签名发送。失败投递会指数退避重试,最多 8 次后标记为失败并保留错误摘要。

安全

  • 密码哈希使用 PBKDF2-SHA256 / 100000 iterations,带每用户随机 16 字节 salt,旧版 salt.hash 哈希也能继续验证(便于无缝升级)。
  • HTTP Signature(rsa-sha256)出站:签名 (request-target)hostdatedigest,自动计算 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 持久表
  • migrations/0006_cached_status_metadata.sql — 远端缓存嘟文的可见性 / mentions / tags / 本地收件人元数据
  • migrations/0007_polls_lists_push_scheduled.sql — poll / list / push subscription / scheduled statuses
  • migrations/0008_outgoing_deliveries.sql — 出站 ActivityPub 投递队列 / 重试状态
  • migrations/0009_markers.sql — Mastodon 读位 markers(home / notifications)
  • migrations/0010_status_edits.sql — 本地嘟文原文 source_text 与 edited_at,支持客户端编辑

重要限制

这是一个单用户可运行实现,不是完整 Mastodon 服务端:

  • 只支持单管理员账号自动初始化,不开放注册
  • 本地状态的可见性已做基础控制:
    • public / unlisted 可公开读取; ActivityPub outbox 只暴露这两类状态
    • private 会按 followers-only 投递,本地读取限作者和本地关注者
    • direct 仍没有完整受众表,本地读取保守限制为作者可见,不应当作为完整私信系统使用
  • 远端嘟文缓存从入站 Create(Note)、已缓存嘟文的 Update(Note) 和搜索远端嘟文 URL 时按需写入,不抓取历史 outbox
  • 远端缓存嘟文会保留正文、CW、语言、可见性、mentions、tags、本地收件人和附件; 互动计数、poll、card 等扩展信息不会完整恢复
  • Web Push 目前实现订阅存储和 API 兼容,尚未实现 VAPID 加密投递通知
  • Poll 当前只在本地 Mastodon API 中序列化和投票,不会联邦成 ActivityPub Question
  • 媒体上传只支持 image/jpegimage/pngimage/gifimage/webp,单文件 10MB,单条状态的附件数量不做服务端限制; 头像和封面同样只按图片路径处理
  • 没有实现接口级限流、反滥用或审核流; follow_requests 相关接口仍是 stub

参考