ActivityPub 协议的简单实现
Aaron Swartz 于十年前的这个时候自杀了。他起草的 RSS (1.0) 协议、和 John Gruber 一起设计、创造的 Markdown 至今一直拥有大量互联网用户。这十年间互联网并没有因他的离世而产生 Open Web 原教旨主义者所期待的愿景。类似「剑桥分析公司」的事情你我都有耳闻。万维网的发明人 Tim Berners-Lee 博士后来提出了 SoLiD 项目 —— 通过将用户数据和应用彻底分离,来实现用户对自身数据的完全掌控。ActivityPub 协议与之类似,但仅面向社交网站。如今,ActivityPub 已经成为了 W3C 的推荐标准;Elon Musk 收购 Twitter 公司之后,由于 "Hardcore Software Engineering" 所展露出的负外部性,Mastodon (长毛象)成为了最火热的分布式/去中心化社交网络平台,而 Mastodon 正是 ActivityPub 的实现之一。这个 Implementation Report 页面展示了一些实现了 ActivityPub 协议的网站列表。
折腾了几天,终于在百忙之中将这个小小的网站基本实现了 ActivityPub 最主要的接口。下面简单梳理一下大致实现的 Server to Server 接口,这些接口对于一个静态博客足矣。注意,这不是一篇严肃教程,仅仅是一些基于个人实现时的简单概括。
本站点实现 ActivityPub 的所有 REST API 均系由 ▲ Vercel Serverless Function (JavaScript) 驱动。
WebFinger
此 API 的定义参考 RFC 7033。这个 WebFinger 协议目的是提供一种针对单个域名的用户发现方式。考虑到此 API 必须使用 Content-Type: application/jrd+json
作为 HTTP 的报文响应类型,因此不推荐直接使用静态文件托管 JSON,请使用 REST API 来构建此实现。
subject
中的 URI 内容后半段和电子邮件非常像 —— ActivityPub 最终的实现效果也和电子邮件类似!
links
中会添加上我们即将要实现的 Actor API。
除此 WebFinger 之外,以下所有 API 都必须设置 Content-Type: application/activity+json
作为响应头。ActivityPub 服务端(比如一个 Mastodon 实例)都会在请求头使用 Accept: application/activity+json
类似的形式来要求我们的实例返回对应的报文格式。
Actor
Actor 就是 Activity 的参与者。WebFinger 会暴露此用户信息 (Profile) 接口。通过此 API,可以告知 ActivityPub 所有关于此用户的其他 API Endpoint,比如用户的 Outbox、Inbox、Followers 等等。所以这些 API 的具体 URL 都可以由自己去定义,而非一成不变。
除此之外,需要提供用户的 PublicKey 来验明身份。我们只需要在自己本地生成一对密钥就可以了。服务端通信中,发往不同 ActivityPub 的实例 HTTPS 请求都需要经过密钥加密。
Outbox
类似 RSS/JSON Feed, 类型为 OrderedCollection
,必须按照时间顺序将最新内容放在 orderedItems
的最前。
OrderedItems 数组中的单个 Item (一般为 Note) 可以是如下形式:
在 ActivityPub 中,所有的对象都必须要提供一个 id
来作为唯一的全局标识符。而且,这个 id 必须是公开可访问的 URI,即可以通过此 id 来访问到此资源对象本身。 例上述如 Outbox 中的一项 Note 可以通过如下 curl 请求得到:
而如果你用浏览器直接打开这个 URL,你将会看到的是一个网页。原因就在于 Accept
这个请求头。
Inbox
本质是一个必须支持 POST 请求的 WebHook。当联邦宇宙中其他用户对你的内容作出了一些交互(比如关注、回复、收藏、转发、删除等操作),会触发此 WebHook。你需要根据 Activity 的类型去处理这些 Payload。一般来说,我们会使用自己的数据库来配合 Inbox Message 做 CRUD。
数据存在自己的数据库之后,你就可以直接在自己的站点上去展示它们。要保持数据于联邦宇宙中的一致性,你需要处理好所有消息类型,并做到接口的幂等 —— 因为 Mastodon 实例会有重试机制。
Followers
关注者列表 API。当 Inbox 接收到来自其他用户的关注请求时,可以获取用户账户后保存到数据库然后通过此 API 展示出来。类型为 OrderedCollection
。也是最简单的一个接口。
Note / Article
需要针对实现 Outbox 中的每一个 orderedItem
的 id
中的 URI 实现一个 JSON 输出。形式可以和 Outbox 中单个 Item 保持一致。
除了 Note
之外,ActivityPub 可以有其他类型的资源,比如长文章的 Article
、视频资源 Video
。不同 ActivityPub 的实现平台对不同资源的展示方式不尽相同。
我的博客页面地址和对应 Activity ID 的 URI 在 URL 形式上保持了一致。因此在实现此 API 后,用户可以在任何 Mastodon 实例的搜索栏中通过搜索我的博客文章页地址来发现它对应的 Mastodon 贴文(由 Outbox 生成);在完全实现 Inbox 后,对贴文的交互数据就能够展示在我的网站上。比如文章页面最下方的 Replies
。
To Do
我的站点没有完全实现所有 ActivityPub 协议,比如 Inbox 消息目前仅处理了 Create Note 和 Accept Follower,还有许多消息类型亟待实现;大部分接受 GET 请求的接口也应当适当配置缓存;Inbox 要严格验证发送者的密钥。
社区实现
很巧合地发现 Cloudflare 也在同一时间段开发了兼容 Mastodon 的 ActivityPub 实现:WildeBeest,有兴趣可以直接用他们的商业化技术栈来部署一个小型实例,或者直接参考他们的代码,用自己擅长的服务端语言实现自己的 ActivityPub。