修复远程缓存
This commit is contained in:
+79
-12
@@ -59,6 +59,8 @@ import type { ParsedBody } from "./http";
|
||||
import type {
|
||||
ActorCache,
|
||||
CachedStatus,
|
||||
CachedStatusMention,
|
||||
CachedStatusTag,
|
||||
Follow,
|
||||
Media,
|
||||
Mention,
|
||||
@@ -97,6 +99,7 @@ type StatusViewer = {
|
||||
user: User | null;
|
||||
actor: string | null;
|
||||
followsByOwnerId: Map<string, boolean>;
|
||||
remoteFollowsByActorId: Map<string, boolean>;
|
||||
};
|
||||
|
||||
function parseRedirectUris(value: string): string[] {
|
||||
@@ -477,10 +480,13 @@ export async function accountStatuses(request: Request, env: Env, accountId: str
|
||||
const remote = await getActorByLocalId(env, accountId)
|
||||
?? (accountId.startsWith("http://") || accountId.startsWith("https://") ? await resolveRemoteActor(env, accountId) : null);
|
||||
if (remote) {
|
||||
const viewer = await loadStatusViewer(request, env);
|
||||
const fetchLimit = Math.min(limit * 4, 160);
|
||||
const rows = await env.DB.prepare(
|
||||
"SELECT * FROM cached_statuses WHERE actor = ? ORDER BY published DESC LIMIT ?"
|
||||
).bind(remote.id, limit).all<CachedStatus>();
|
||||
const items = await Promise.all(rows.results.map((row) => cachedStatusToMastodon(env, row)));
|
||||
).bind(remote.id, fetchLimit).all<CachedStatus>();
|
||||
const visibleRows = await filterCachedStatusesForViewer(env, rows.results, viewer);
|
||||
const items = await Promise.all(visibleRows.slice(0, limit).map((row) => cachedStatusToMastodon(env, row)));
|
||||
return json(items);
|
||||
}
|
||||
|
||||
@@ -920,6 +926,8 @@ export async function homeTimeline(request: Request, env: Env): Promise<Response
|
||||
const user = await requireUser(request, env);
|
||||
const url = new URL(request.url);
|
||||
const limit = clampLimit(url.searchParams.get("limit"), 20, 40);
|
||||
const viewer = statusViewerForUser(env, user);
|
||||
const cachedFetchLimit = Math.min(limit * 4, 160);
|
||||
|
||||
const localRows = await env.DB.prepare(
|
||||
"SELECT * FROM statuses WHERE user_id = ? ORDER BY created_at DESC LIMIT ?"
|
||||
@@ -930,10 +938,11 @@ export async function homeTimeline(request: Request, env: Env): Promise<Response
|
||||
INNER JOIN outgoing_follows of ON of.target_actor = cs.actor
|
||||
WHERE of.local_user_id = ? AND of.accepted = 1
|
||||
ORDER BY cs.published DESC LIMIT ?`
|
||||
).bind(user.id, limit).all<CachedStatus>();
|
||||
).bind(user.id, cachedFetchLimit).all<CachedStatus>();
|
||||
|
||||
const localItems = await serializeStatuses(env, localRows.results, request);
|
||||
const cachedItems = await Promise.all(cachedRows.results.map((row) => cachedStatusToMastodon(env, row)));
|
||||
const visibleCachedRows = await filterCachedStatusesForViewer(env, cachedRows.results, viewer);
|
||||
const cachedItems = await Promise.all(visibleCachedRows.slice(0, limit).map((row) => cachedStatusToMastodon(env, row)));
|
||||
|
||||
const merged = [...localItems, ...cachedItems].sort((a, b) => {
|
||||
const at = String(a.created_at ?? "");
|
||||
@@ -1219,18 +1228,20 @@ async function cachedStatusToMastodon(env: Env, row: CachedStatus): Promise<Reco
|
||||
const cache = await resolveRemoteActor(env, row.actor);
|
||||
const account = cache ? remoteAccountJson(cache) : { id: row.actor, acct: row.actor, username: row.actor };
|
||||
const attachments = await listCachedStatusAttachments(env, row.id);
|
||||
const mentions = parseCachedJson<CachedStatusMention>(row.mentions_json);
|
||||
const tags = parseCachedJson<CachedStatusTag>(row.tags_json);
|
||||
return {
|
||||
id: row.object_id,
|
||||
uri: row.object_id,
|
||||
url: row.url,
|
||||
account,
|
||||
in_reply_to_id: null,
|
||||
in_reply_to_id: row.in_reply_to,
|
||||
in_reply_to_account_id: null,
|
||||
content: row.content,
|
||||
text: row.content,
|
||||
created_at: row.published,
|
||||
edited_at: null,
|
||||
visibility: "public",
|
||||
visibility: row.visibility,
|
||||
language: row.language,
|
||||
sensitive: Boolean(row.sensitive),
|
||||
spoiler_text: row.summary,
|
||||
@@ -1247,8 +1258,13 @@ async function cachedStatusToMastodon(env: Env, row: CachedStatus): Promise<Reco
|
||||
description: att.description,
|
||||
blurhash: null
|
||||
})),
|
||||
mentions: [],
|
||||
tags: [],
|
||||
mentions: mentions.map((mention) => ({
|
||||
id: mention.actor,
|
||||
username: mention.acct.replace(/^@/, "").split("@")[0],
|
||||
acct: mention.acct.replace(/^@/, ""),
|
||||
url: mention.url
|
||||
})),
|
||||
tags: tags.map((tag) => ({ name: tag.name, url: tag.url })),
|
||||
emojis: [],
|
||||
reblogs_count: 0,
|
||||
favourites_count: 0,
|
||||
@@ -1265,6 +1281,15 @@ async function cachedStatusToMastodon(env: Env, row: CachedStatus): Promise<Reco
|
||||
};
|
||||
}
|
||||
|
||||
function parseCachedJson<T>(value: string): T[] {
|
||||
try {
|
||||
const parsed = JSON.parse(value) as unknown;
|
||||
return Array.isArray(parsed) ? parsed as T[] : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function statusJson(
|
||||
env: Env,
|
||||
status: Status,
|
||||
@@ -1589,6 +1614,12 @@ async function loadStatusesByIds(env: Env, statusIds: string[]): Promise<Status[
|
||||
return rows.results;
|
||||
}
|
||||
|
||||
async function loadCachedStatusesByObjectIds(env: Env, objectIds: string[]): Promise<CachedStatus[]> {
|
||||
if (objectIds.length === 0) return [];
|
||||
const rows = await env.DB.prepare(`SELECT * FROM cached_statuses WHERE object_id IN (${placeholders(objectIds.length)})`).bind(...objectIds).all<CachedStatus>();
|
||||
return rows.results;
|
||||
}
|
||||
|
||||
async function loadMediaByStatusIds(env: Env, statusIds: string[]): Promise<Map<string, Media[]>> {
|
||||
const grouped = new Map<string, Media[]>();
|
||||
if (statusIds.length === 0) return grouped;
|
||||
@@ -1666,10 +1697,18 @@ async function loadReplyCountByStatusIds(env: Env, statusIds: string[]): Promise
|
||||
async function serializeNotifications(env: Env, notifications: Notification[], request: Request): Promise<Record<string, unknown>[]> {
|
||||
if (notifications.length === 0) return [];
|
||||
|
||||
const statuses = await loadStatusesByIds(env, uniqueStrings(notifications.map((notification) => notification.status_id)));
|
||||
const visibleStatuses = await filterStatusesForViewer(env, statuses, await loadStatusViewer(request, env));
|
||||
const notificationStatusIds = uniqueStrings(notifications.map((notification) => notification.status_id));
|
||||
const viewer = await loadStatusViewer(request, env);
|
||||
const statuses = await loadStatusesByIds(env, notificationStatusIds);
|
||||
const localStatusIds = new Set(statuses.map((status) => status.id));
|
||||
const cachedStatuses = await loadCachedStatusesByObjectIds(env, notificationStatusIds.filter((statusId) => !localStatusIds.has(statusId)));
|
||||
const visibleStatuses = await filterStatusesForViewer(env, statuses, viewer);
|
||||
const visibleCachedStatuses = await filterCachedStatusesForViewer(env, cachedStatuses, viewer);
|
||||
const serializedStatuses = await serializeStatuses(env, visibleStatuses, request);
|
||||
const serializedStatusById = new Map(serializedStatuses.map((item) => [String(item.id), item]));
|
||||
const serializedCachedStatuses = await Promise.all(visibleCachedStatuses.map((row) => cachedStatusToMastodon(env, row)));
|
||||
const serializedStatusById = new Map(
|
||||
[...serializedStatuses, ...serializedCachedStatuses].map((item) => [String(item.id), item])
|
||||
);
|
||||
|
||||
const remoteActorIds = uniqueStrings(
|
||||
notifications.map((notification) => notification.actor).filter((actorId) => !actorId.startsWith(baseUrl(env)))
|
||||
@@ -1762,7 +1801,8 @@ function statusViewerForUser(env: Env, user: User | null): StatusViewer {
|
||||
return {
|
||||
user,
|
||||
actor: user ? actorUrl(env, user) : null,
|
||||
followsByOwnerId: new Map()
|
||||
followsByOwnerId: new Map(),
|
||||
remoteFollowsByActorId: new Map()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1786,6 +1826,14 @@ async function filterStatusesForViewer(env: Env, statuses: Status[], viewer: Sta
|
||||
return visible;
|
||||
}
|
||||
|
||||
async function filterCachedStatusesForViewer(env: Env, statuses: CachedStatus[], viewer: StatusViewer): Promise<CachedStatus[]> {
|
||||
const visible: CachedStatus[] = [];
|
||||
for (const status of statuses) {
|
||||
if (await canViewerViewCachedStatus(env, status, viewer)) visible.push(status);
|
||||
}
|
||||
return visible;
|
||||
}
|
||||
|
||||
async function canViewerViewStatus(env: Env, status: Status, viewer: StatusViewer): Promise<boolean> {
|
||||
if (status.visibility === "public" || status.visibility === "unlisted") return true;
|
||||
if (viewer.user?.id === status.user_id) return true;
|
||||
@@ -1793,6 +1841,13 @@ async function canViewerViewStatus(env: Env, status: Status, viewer: StatusViewe
|
||||
return false;
|
||||
}
|
||||
|
||||
async function canViewerViewCachedStatus(env: Env, status: CachedStatus, viewer: StatusViewer): Promise<boolean> {
|
||||
if (status.visibility === "public" || status.visibility === "unlisted") return true;
|
||||
if (!viewer.actor) return false;
|
||||
if (status.visibility === "private") return viewerFollowsRemoteActor(env, viewer, status.actor);
|
||||
return parseCachedJson<string>(status.local_recipients_json).includes(viewer.actor);
|
||||
}
|
||||
|
||||
async function viewerFollowsOwner(env: Env, viewer: StatusViewer, ownerUserId: string): Promise<boolean> {
|
||||
if (!viewer.actor) return false;
|
||||
const cached = viewer.followsByOwnerId.get(ownerUserId);
|
||||
@@ -1805,6 +1860,18 @@ async function viewerFollowsOwner(env: Env, viewer: StatusViewer, ownerUserId: s
|
||||
return follows;
|
||||
}
|
||||
|
||||
async function viewerFollowsRemoteActor(env: Env, viewer: StatusViewer, actorId: string): Promise<boolean> {
|
||||
if (!viewer.user) return false;
|
||||
const cached = viewer.remoteFollowsByActorId.get(actorId);
|
||||
if (cached !== undefined) return cached;
|
||||
const row = await env.DB.prepare(
|
||||
"SELECT 1 AS hit FROM outgoing_follows WHERE local_user_id = ? AND target_actor = ? AND accepted = 1 LIMIT 1"
|
||||
).bind(viewer.user.id, actorId).first<{ hit: number }>();
|
||||
const follows = Boolean(row?.hit);
|
||||
viewer.remoteFollowsByActorId.set(actorId, follows);
|
||||
return follows;
|
||||
}
|
||||
|
||||
async function viewerUser(request: Request, env: Env): Promise<User | null> {
|
||||
const auth = request.headers.get("authorization") ?? "";
|
||||
const token = auth.match(/^Bearer\s+(.+)$/i)?.[1];
|
||||
|
||||
Reference in New Issue
Block a user