第 5 章:ActivityPub 联邦协议
一、联邦的本质:把本地领域事件翻译成网络活动
Mastodon 的本地模型不直接暴露给远端。它把本地的“关注、发帖、编辑、点赞、转发、删除”翻译成 ActivityPub Activity,并通过 HTTP POST 投递到远端 inbox。反过来,远端 Activity 进入本实例后,经过签名验证、解析、类型分派和本地 service,才变成本地 Account/Status/Follow 等事实。
本地事实 → ActivityPub serializer → HTTP Signature → 远端 inbox
远端 JSON → Fetch/Dereference → Verify → ActivityPub::Activity::* → 本地 service
二、三个基础对象
Actor
一个本地或远端账户的联邦身份,包含 id、inbox、outbox、followers、公钥、头像和 profile。Account 是数据库表示,Actor JSON 是协议表示。
Object
状态通常是 Note,媒体、emoji、collection 也有自己的对象形状。对象的 id 是 URI,不能简单当成本地整数 ID。
Activity
Create、Update、Delete、Follow、Accept、Reject、Like、Announce、Undo 等活动描述“对对象做了什么”。处理器根据 activity type 进入对应的 subclass。
三、出站链路:从发帖到远端 inbox
sequenceDiagram
participant S as PostStatusService
participant D as AP DistributionWorker
participant A as ActivityPub Serializer
participant W as DeliveryWorker
participant R as Remote Inbox
participant T as FailureTracker
S->>D: status id
D->>A: build Create activity
A-->>D: JSON-LD payload
D->>W: source account + inbox + json
W->>W: Stoplight + RequestPool
W->>R: signed POST application/activity+json
R-->>W: 2xx / 4xx / 5xx / timeout
W->>T: success or failure state
1. DistributionWorker 选择收件人
它会根据状态 visibility、作者 followers、远端 inbox、shared inbox 和是否需要同步 followers,决定发送给哪些目标。不能简单把“所有 followers”展开成 URL 列表,因为多个账户可能共享同一个 shared inbox。
2. Serializer 生成协议对象
app/serializers/activitypub 负责 actor、Note、Create 和 collection 形状。它需要把本地关系映射成 URI,把可见性映射成 to/cc audience,并处理媒体、mention、quote、敏感标记和编辑版本。
3. DeliveryWorker 负责可靠 HTTP
ActivityPub::DeliveryWorker 的输入很小:JSON、source account id、inbox URL 和 options。执行时:
- 检查 inbox 是否处于 failure tracker 的可用状态;
- 通过 source account 的 keypair 签名;
- 设置
application/activity+json; - 使用 request pool 复用按 host 的 HTTP client;
- 对可重试错误抛出异常,让 Sidekiq retry;
- 对不可恢复错误标记为 unsalvageable;
- 成功或失败后更新 delivery tracker。
四、Stoplight 与重试为何同时存在
单个远端服务器持续失败时,盲目重试会浪费线程和连接,也会把本实例的队列拖慢。Stoplight 在一个 host 连续失败后进入 cool-off,暂时跳过投递;Sidekiq retry 则负责稍后再次尝试。
第一次超时 → job retry
连续多次失败 → Stoplight open
冷却期结束 → 允许试探请求
成功 → 恢复 closed
持续 4xx/不可恢复授权错误 → 标记 unsalvageable
这是一种“按远端 host 隔离失败”的策略:坏邻居不会让所有联邦投递一起阻塞。
五、入站链路:远端活动如何变成本地事实
POST /users/:username/inbox
├─ 解析 Content-Type 与 JSON-LD
├─ 找到 actor / key id
├─ FetchRemoteKeyService / Dereferencer
├─ 验证 HTTP Signature / digest / authorized actor
├─ 交给 ActivityPub::Processing
├─ 按 type 分派 ActivityPub::Activity::*
└─ enqueue / save / notify / follow-up delivery
验证层
入站 JSON 来自不可信网络。系统需要确认:
- 请求签名对应发送 actor;
- actor 的公钥可获取且未被篡改;
- actor 允许执行当前 activity;
- 对象 URI、域名和来源关系符合联邦规则;
- 不能因为一个远端 payload 直接覆盖本地受保护字段。
解析层
ActivityPub::Parser 下的 parser 把不同实现的字段差异转换成 Mastodon 内部结构,例如 status、media attachment、poll、mention、custom emoji 和 preview card。解析是兼容性边界,不能把远端 JSON 当成可信的本地模型属性。
Activity subclass
ActivityPub::Activity::Create、Follow、Accept、Undo、Delete 等类实现 type-specific 语义。共同的 actor/object 验证放在基类,特殊权限和副作用放在子类。
六、Fetch 与 Process 的分工
当用户在本实例搜索或打开远端 URI 时,可能需要先 fetch remote actor/status;当远端主动 POST Activity 时,则是 process inbound activity。两条路径都可能调用 dereferencer,但触发场景不同:
| 路径 | 触发方 | 主要工作 |
|---|---|---|
| Fetch | 本地用户/本地 service | 获取远端 JSON、建立/更新本地副本 |
| Process | 远端服务器 | 验证并应用对方发送的 Activity |
缓存、超时、重试和 SSRF 防护必须覆盖两条路径。
七、URI 与本地 ID 的双重身份
本地 status 有数据库 ID,也有 canonical URI。远端 status 可能只有 URI,后来才被导入并分配本地 ID。因此:
- 跨实例去重应优先使用 URI/对象 ID;
- 本地查询可以使用 integer/UUID 主键;
- serializer 不能把本地 ID 当成联邦 ID;
- 删除、更新和引用必须能通过 URI 找到既有副本;
- 迁移和重试必须避免同一远端对象重复创建。
八、隐私不是只靠 UI
私密/仅提及状态会影响多个协议字段:
- 本地 REST 是否能被当前 viewer 读取;
- 本地 Feed 哪些账户收到;
- ActivityPub
to/cc与 followers collection; - remote inbox 是否应该收到;
- quote/reply 是否允许继续传播;
- serializer 是否隐藏敏感字段。
因此修改 visibility 相关代码时,必须同时跑 request、model、serializer、ActivityPub 和 worker specs,不能只补一个 Controller spec。
九、联邦排障路线
“远端看不到我的状态”
- 看状态是否成功落库;
- 看
ActivityPub::DistributionWorker是否入队; - 看 recipient/inbox 是否正确;
- 看
DeliveryWorker是否被 Stoplight 跳过; - 看远端响应码和 body;
- 看签名 key、clock skew、TLS、content type;
- 最后再看远端是否异步处理或把 activity 丢到 moderation。
“本地没有远端回复”
- 确认远端请求进入正确 inbox;
- 验证签名是否通过;
- 找 Activity subclass 和 parser 日志;
- 检查对象 URI dereference 是否失败;
- 检查本地账户是否被 suspend/block;
- 检查 notification/feed fan-out 是否延迟。
十、源码入口清单
app/lib/activitypub/activity.rbapp/lib/activitypub/activity/create.rbapp/services/activitypub/process_activity_service.rbapp/services/activitypub/fetch_remote_actor_service.rbapp/workers/activitypub/delivery_worker.rbapp/lib/activitypub/dereferencer.rbapp/lib/activitypub/linked_data_signature.rb
十一、小结
ActivityPub 层的核心是“不可信网络上的最终一致性”。serializer 负责翻译,签名负责来源,parser 负责兼容,Activity subclass 负责语义,worker + Stoplight 负责可靠传播。下一章看同一套事件如何通过 Redis 进入 WebSocket。