部落格作為一個fediverse帳號 [8664]

延續部落格問題挑戰的規劃,我研究了怎麼讓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訊息在公開端點不應該列出

把互動放到文章裡面 [local-5]

有一個公開端點 GET /interactions/:addr 可以看到這篇文章的互動,所以blog這邊用一支 interactions.js 根據 location.pathname 作為addr去問,有互動資料才顯示fediverse區塊參見部落格問題挑戰2026-08-18 · Lîm Tsú-thuàn

不受信任的HTML不應該進 innerHTML,所以先解析成一個不會執行的document,只把文字讀回來

最後是CORS。因為這個端點不吃憑證、也只回公開的資料,所以worker那邊的端點填 *

Access-Control-Allow-Origin: *