修复远程缓存

This commit is contained in:
浪子
2026-05-14 15:36:25 +08:00
parent 5365a3569f
commit a2badc2d4f
6 changed files with 225 additions and 33 deletions
+95 -15
View File
@@ -35,7 +35,7 @@ import {
PUBLIC_COLLECTION,
SECURITY_CONTEXT
} from "./types";
import type { ActorCache, Json, RemoteActor, Status, User } from "./types";
import type { ActorCache, CachedStatus, CachedStatusMention, CachedStatusTag, Json, RemoteActor, Status, User } from "./types";
import {
actorUrl,
activityUrl,
@@ -419,7 +419,7 @@ async function handleUpdate(ctx: InboxContext): Promise<Response> {
if (String(obj.type ?? "") === "Note" && typeof obj.id === "string") {
const existing = await getCachedStatusByObjectId(env, obj.id);
if (existing && existing.actor === actorId) {
await cacheRemoteNote(env, actorId, obj);
await cacheRemoteNote(env, actorId, obj, activity.body, existing);
}
}
return new Response(null, { status: 202 });
@@ -439,28 +439,27 @@ async function handleCreate(ctx: InboxContext): Promise<Response> {
}
const isPublic = recipients.includes(PUBLIC_COLLECTION);
const isFollowersOnly = recipients.some((recipient) => isFollowersCollection(actorId, recipient));
const followed = await isFollowedByAnyLocalUser(env, actorId);
if (followed && (isPublic || localActorIds.size > 0)) {
await cacheRemoteNote(env, actorId, obj);
if ((followed && (isPublic || isFollowersOnly)) || localActorIds.size > 0) {
await cacheRemoteNote(env, actorId, obj, activity.body);
}
for (const username of localActorIds) {
const localUser = await getUserByUsername(env, username);
if (!localUser) continue;
const inReplyTo = typeof obj.inReplyTo === "string" ? obj.inReplyTo : null;
let statusId: string | null = null;
if (inReplyTo) {
const parent = await getStatusByObjectId(env, inReplyTo);
if (parent) statusId = parent.id;
}
await recordNotification(env, localUser.id, "mention", actorId, statusId);
await recordNotification(env, localUser.id, "mention", actorId, typeof obj.id === "string" ? obj.id : null);
}
return new Response(null, { status: 202 });
}
async function cacheRemoteNote(env: Env, actorId: string, note: Json): Promise<void> {
async function cacheRemoteNote(env: Env, actorId: string, note: Json, activity: Json = {}, fallback?: CachedStatus): Promise<void> {
if (typeof note.id !== "string") return;
const cachedId = note.id;
const recipients = collectRecipients(activity, note);
const mentions = note.tag === undefined ? parseJsonArray<CachedStatusMention>(fallback?.mentions_json) : extractRemoteMentions(note);
const tags = note.tag === undefined ? parseJsonArray<CachedStatusTag>(fallback?.tags_json) : extractRemoteHashtags(note);
const localRecipients = recipients.length === 0 ? parseJsonArray<string>(fallback?.local_recipients_json) : collectLocalRecipients(env, recipients);
const stored = await upsertCachedStatus(env, {
id: cachedId,
object_id: cachedId,
@@ -469,9 +468,13 @@ async function cacheRemoteNote(env: Env, actorId: string, note: Json): Promise<v
summary: typeof note.summary === "string" ? note.summary : "",
sensitive: note.sensitive ? 1 : 0,
language: typeof note.contentMap === "object" && note.contentMap ? Object.keys(note.contentMap as Json)[0] ?? "en" : "en",
visibility: inferRemoteVisibility(actorId, activity, note, fallback?.visibility ?? "public"),
in_reply_to: typeof note.inReplyTo === "string" ? note.inReplyTo : null,
url: typeof note.url === "string" ? note.url : cachedId,
published: typeof note.published === "string" ? note.published : new Date().toISOString()
published: typeof note.published === "string" ? note.published : new Date().toISOString(),
mentions_json: JSON.stringify(mentions),
tags_json: JSON.stringify(tags),
local_recipients_json: JSON.stringify(localRecipients)
});
if (!stored) return;
await env.DB.prepare("DELETE FROM cached_status_attachments WHERE cached_status_id = ?").bind(stored.id).run();
@@ -494,8 +497,85 @@ async function cacheRemoteNote(env: Env, actorId: string, note: Json): Promise<v
}
}
function inferRemoteVisibility(actorId: string, activity: Json, object: Json, fallback: string): string {
const to = collectRecipientFields(activity.to, activity.bto, object.to, object.bto);
const cc = collectRecipientFields(activity.cc, activity.bcc, object.cc, object.bcc);
const recipients = new Set([...to, ...cc]);
if (recipients.size === 0) return fallback;
if (to.has(PUBLIC_COLLECTION)) return "public";
if (cc.has(PUBLIC_COLLECTION)) return "unlisted";
if ([...recipients].some((recipient) => isFollowersCollection(actorId, recipient))) return "private";
return "direct";
}
function isFollowersCollection(actorId: string, recipient: string): boolean {
return recipient === `${actorId}/followers` || recipient.endsWith("/followers");
}
function collectLocalRecipients(env: Env, recipients: string[]): string[] {
return recipients.filter((recipient) => recipient.startsWith(baseUrl(env)) && /\/users\/[^/?#]+$/.test(recipient));
}
function extractRemoteMentions(note: Json): CachedStatusMention[] {
const mentions: CachedStatusMention[] = [];
for (const tag of noteTagObjects(note)) {
if (String(tag.type ?? "") !== "Mention") continue;
const url = stringValue(tag.href) ?? stringValue(tag.id);
if (!url) continue;
const acct = mentionAcct(tag, url);
mentions.push({ actor: url, acct, url });
}
return mentions;
}
function extractRemoteHashtags(note: Json): CachedStatusTag[] {
const tags: CachedStatusTag[] = [];
for (const tag of noteTagObjects(note)) {
const name = stringValue(tag.name);
if (String(tag.type ?? "") !== "Hashtag" && !name?.startsWith("#")) continue;
if (!name) continue;
tags.push({ name: name.replace(/^#/, "").toLowerCase(), url: stringValue(tag.href) ?? stringValue(tag.id) });
}
return tags;
}
function noteTagObjects(note: Json): Json[] {
const tags = Array.isArray(note.tag) ? note.tag : note.tag ? [note.tag] : [];
return tags.filter((tag): tag is Json => Boolean(tag) && typeof tag === "object");
}
function mentionAcct(tag: Json, url: string): string {
const name = stringValue(tag.name)?.replace(/^@/, "");
if (name) return name;
try {
const parsed = new URL(url);
const username = parsed.pathname.split("/").filter(Boolean).pop() ?? parsed.host;
return `${username}@${parsed.host}`;
} catch {
return url;
}
}
function stringValue(value: unknown): string | null {
return typeof value === "string" && value ? value : null;
}
function parseJsonArray<T>(value: string | null | undefined): T[] {
if (!value) return [];
try {
const parsed = JSON.parse(value) as unknown;
return Array.isArray(parsed) ? parsed as T[] : [];
} catch {
return [];
}
}
function collectRecipients(activity: Json, object: Json): string[] {
const fields: unknown[] = [activity.to, activity.cc, activity.bto, activity.bcc, object.to, object.cc];
const fields: unknown[] = [activity.to, activity.cc, activity.bto, activity.bcc, object.to, object.cc, object.bto, object.bcc];
return [...collectRecipientFields(...fields)];
}
function collectRecipientFields(...fields: unknown[]): Set<string> {
const out = new Set<string>();
for (const field of fields) {
if (Array.isArray(field)) {
@@ -506,7 +586,7 @@ function collectRecipients(activity: Json, object: Json): string[] {
out.add(field);
}
}
return [...out];
return out;
}
async function localUserFromTarget(env: Env, actorId: string | null): Promise<User | null> {