第 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

一个本地或远端账户的联邦身份,包含 idinboxoutboxfollowers、公钥、头像和 profile。Account 是数据库表示,Actor JSON 是协议表示。

Object

状态通常是 Note,媒体、emoji、collection 也有自己的对象形状。对象的 id 是 URI,不能简单当成本地整数 ID。

Activity

CreateUpdateDeleteFollowAcceptRejectLikeAnnounceUndo 等活动描述“对对象做了什么”。处理器根据 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::CreateFollowAcceptUndoDelete 等类实现 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

私密/仅提及状态会影响多个协议字段:

  1. 本地 REST 是否能被当前 viewer 读取;
  2. 本地 Feed 哪些账户收到;
  3. ActivityPub to/cc 与 followers collection;
  4. remote inbox 是否应该收到;
  5. quote/reply 是否允许继续传播;
  6. serializer 是否隐藏敏感字段。

因此修改 visibility 相关代码时,必须同时跑 request、model、serializer、ActivityPub 和 worker specs,不能只补一个 Controller spec。

九、联邦排障路线

“远端看不到我的状态”

  1. 看状态是否成功落库;
  2. ActivityPub::DistributionWorker 是否入队;
  3. 看 recipient/inbox 是否正确;
  4. DeliveryWorker 是否被 Stoplight 跳过;
  5. 看远端响应码和 body;
  6. 看签名 key、clock skew、TLS、content type;
  7. 最后再看远端是否异步处理或把 activity 丢到 moderation。

“本地没有远端回复”

  1. 确认远端请求进入正确 inbox;
  2. 验证签名是否通过;
  3. 找 Activity subclass 和 parser 日志;
  4. 检查对象 URI dereference 是否失败;
  5. 检查本地账户是否被 suspend/block;
  6. 检查 notification/feed fan-out 是否延迟。

十、源码入口清单

十一、小结

ActivityPub 层的核心是“不可信网络上的最终一致性”。serializer 负责翻译,签名负责来源,parser 负责兼容,Activity subclass 负责语义,worker + Stoplight 负责可靠传播。下一章看同一套事件如何通过 Redis 进入 WebSocket。