延續部落格問題挑戰的規劃,我研究了怎麼讓blog作為fediverse帳號存在。我希望部落格文章會得到一個fediverse post,有人在fediverse上跟post互動時,可以在部落格本體看到變化。這樣就等於借用了fediverse作為留言系統
這個專案公開端點是 https://fedi.dannypsnl.me,借用大量的Cloudflare(CF)功能與Fedify完成:
- D1:跟隨者、貼文、互動
- KV:Fedify的快取
- Queue:投遞跟收信
- R2:貼文附圖的儲存
ActivityPub需要什麼? [local-0]
一個fediverse帳號是一個HTTP服務,要能回答這幾件事:
/@blog:actor,帳號本身/@blog/inbox:別人寄東西來的地方/@blog/outbox、followers、following:這個帳號的公開集合/.well-known/webfinger:把@blog@fedi.dannypsnl.me這個名字換成上面那個actor的網址
這些都能用Fedify處理,因為要簽HTTP Signatures,這塊很麻煩所以要盡量復用被驗證過的程式。Actor的宣告:
builder.setActorDispatcher("/@{identifier}", async (ctx, identifier) => {
if (identifeir !== "...") return null;
const keys = await ctx.getActorKeyPairs(identifier);
return new Person({
// ...
publicKey: keys[0].cryptographicKey,
assertionMethods: keys.map((k) => k.multikey),
});
}) Actor的id是永久的 [local-1]
我一開始把Actor放在 /users/blog,後來改成 /@blog。這樣會非常麻煩,因為那個id改掉之後已經追蹤的帳號手上那個id就指向一個404……
補救就要用ActivityPub自己的move功能。舊網址得回一個退休的Actor,用 successor 指向新的;新的Actor用 aliases 認領舊的,兩邊對得起來遠端才會相信。然後從舊id發一個 Move 給所有跟隨者,而且要用舊id的key id去簽
RSS就是資料來源 [local-2]
利用RSS的話,就不需要改任何現有程式。Worker用cron抓取
[triggers] crons = ["0 0 * * *"]
item的 guid 作為識別機制:D1裡貼文的主鍵是它,Note的網址是 /@blog/notes/<addr>,前端要問互動的時候拿 location.pathname
同步功能先建立草稿,不會馬上發佈。因為第一次同步會讀到整個feed,直接發就是把站上所有文章一次丟到別人的timeline上。所以另外建立了一個用CF存取規則保護的admin頁面管理這些草稿
用Queue投遞,這裡有個坑,Queue裡的訊息是包裝過的:
const result = await workersQueue.processMessage(message.body);
if (!result.shouldProcess) { message.retry(); continue; }
await federation.processQueuedTask(env, result.message);
我原本直接把 message.body 丟給 processQueuedTask,它就永遠對不上任何一種task,然後安靜地什麼都不做
新follower的回填 [local-3]
Mastodon不會自己爬你的outbox,它只顯示追蹤之後推給它的活動。所以剛追蹤的人看到的是一個空帳號。因此Follow進來時除了回Accept,還要把已經發過的文章補送給這個人:
const posts = await listPosts(ctx.data, "published");
for (const post of posts) {
await ctx.sendActivity({ identifier: IDENTIFIER }, follower,
buildCreateNote(ctx, IDENTIFIER, post.addr));
}
重送本身是安全的,活動的id是從addr算出來的(/@blog/notes/<addr>#create),fediverse站點看到收過的id就不會再存一次
互動 [local-4]
inbox要處理的東西其實不多
Create帶一個Note:回覆Like:讚Announce:轉發Delete、Undo:把上面存進去的刪掉
回覆只認 inReplyTo 指向我自己的Note的那些,所以一整串討論只會留下直接的reply,而不是整個討論串。那些內容屬於原本的伺服器,我這邊只知道有人對這篇文章說了什麼,剩下的點連結過去看
回覆的可見性是回覆者自己選的,跟被回覆的文章無關。私訊回覆一樣會進inbox,因為Mastodon一定會把被回覆的人列進收件人。所以存的時候要先分類:
function classifyVisibility(toIds, ccIds) {
const pub = PUBLIC_COLLECTION.href;
if (toIds.some((u) => u.href === pub)) return "public";
if (ccIds.some((u) => u.href === pub)) return "unlisted";
return "private";
}
private訊息在公開端點不應該列出