Activity · 第一個請求
發佈貼文並讀取時間軸
這條路徑會為簽署請求的 actor 建立一筆 post,再讀取同一位 actor 的 materialized home timeline。請在能安全存取 gateway secret 的 host server 執行。
1. 序列化並簽署 post
const path = "/api/v1/posts";
const timestamp = Math.floor(Date.now() / 1000).toString();
const body = JSON.stringify({
idempotency_key: "match:match_1842:published",
verb: "matched",
object_ref: { type: "match", id: "match_1842" },
body: "Won the summer qualifier",
metadata: { season: "2026-summer" },
render_refs: {},
visibility: "followers"
});
const signature = signActivityRequest({
secret: process.env.ACTIVITY_GATEWAY_SECRET,
timestamp,
method: "POST",
path,
tenantId,
actorId,
body
}); verb 是必填欄位。每次邏輯發佈都應設定穩定的 idempotency_key。object_ref、metadata 與 render_refs 預設為空 object。visibility 可以是 public、followers 或 private。
2. 發佈 post
const response = await fetch(`${activityUrl}${path}`, {
method: "POST",
headers: {
"content-type": "application/json",
"x-tenant-id": tenantId,
"x-actor-id": actorId,
"x-feedaas-timestamp": timestamp,
"x-feedaas-signature": signature
},
body
});
if (!response.ok) throw new Error(await response.text());
const { post } = await response.json(); 成功寫入會回傳 201 Created。Activity 從簽署 header 取得 actor_id,建立來源 post 與作者自己的 self timeline entry,並在 visibility 允許時排入 follower fan-out。
3. 讀取首頁時間軸
讀取請求必須單獨簽署。它的 method、path、query string、body hash 與 timestamp 都和發佈請求不同。
const path = "/api/v1/timeline/home";
const query = "limit=20";
const timestamp = Math.floor(Date.now() / 1000).toString();
const signature = signActivityRequest({
secret: process.env.ACTIVITY_GATEWAY_SECRET,
timestamp,
method: "GET",
path,
query,
tenantId,
actorId,
body: ""
});
const response = await fetch(`${activityUrl}${path}?${query}`, {
headers: {
"x-tenant-id": tenantId,
"x-actor-id": actorId,
"x-feedaas-timestamp": timestamp,
"x-feedaas-signature": signature
}
});
if (!response.ok) throw new Error(await response.text());
const timeline = await response.json(); Response 會包含 materialized entry 與 opaque next cursor:
{
"data": [
{
"id": "timeline-entry-uuid",
"viewer_id": "player_42",
"reason": "self",
"ranked_at": "2026-07-14T04:10:00Z",
"item": {
"post": {
"id": "post-uuid",
"actor_id": "player_42",
"verb": "matched",
"visibility": "followers"
},
"stats": {
"like_count": 0,
"comment_count": 0,
"reply_count": 0,
"repost_count": 0,
"share_count": 0
},
"viewer": {
"has_liked": false,
"follows_author": true,
"can_delete": true
}
}
}
],
"page": { "next_cursor": null }
} 實際的 post 與 viewer object 會包含 OpenAPI 契約定義的完整欄位。這個範例只保留用來說明 response layer 的部分。
同步完成的部分
作者自己的 self timeline entry 會和 post 一起建立。Follower fan-out 會非同步執行,並在 job 重試後收斂。Post 成功回傳,不代表每一位 follower 的時間軸都已更新。
PostStats 同樣是非同步產生的衍生資料,不能作為 engagement 的 canonical record。
重試邊界
重試同一次邏輯發佈時,請沿用相同的 idempotency_key。在同一個 tenant 與 actor 範圍內,Activity 會回傳原本的 post,不會再次建立 timeline entry、fan-out job 或 outbox event。新的發佈必須使用新的 key。
Timeline read 可以用新簽章安全重試。page.next_cursor 必須視為 opaque value,並將完全相同的 encoded value 放入 request URL 與簽署的 query string。