This commit is contained in:
浪子
2026-05-15 22:40:43 +08:00
parent e2fcf08e2f
commit cba4901058
7 changed files with 205 additions and 9 deletions
+118 -3
View File
@@ -1,6 +1,7 @@
import {
actorDocument,
announceActivity,
cacheRemoteNote,
createActivity,
deleteActivity,
followActivity,
@@ -25,17 +26,20 @@ import {
getActorFromCache,
getAdminUser,
getAppByClientId,
getCachedStatusByObjectId,
getStatus,
getUserById,
getUserByIdOrUsername,
getUserByUsername,
insertOAuthToken,
listCachedStatusAttachments,
listMarkers,
listProfileFields,
recordNotification,
removeBookmark,
removePin,
replaceProfileFields,
saveMarker,
setUserAvatarKey,
setUserHeaderKey,
takeOAuthCode
@@ -43,6 +47,7 @@ import {
import {
deliverToInboxes,
gatherFollowerInboxes,
objectAsJson,
resolveDeliveryInboxes,
resolveRemoteActor
} from "./federation";
@@ -66,6 +71,7 @@ import type {
Json,
Media,
Mention,
Marker,
Notification,
Poll,
PollOption,
@@ -104,6 +110,7 @@ const MAX_POLL_OPTION_CHARS = 50;
const MIN_POLL_EXPIRATION_SECONDS = 300;
const MAX_POLL_EXPIRATION_SECONDS = 2629746;
const SCHEDULED_STATUS_MIN_DELAY_SECONDS = 300;
const ACTIVITY_JSON_ACCEPT = "application/activity+json, application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\", application/json";
type StatusVisibility = "public" | "unlisted" | "private" | "direct";
type StatusViewer = {
@@ -1549,8 +1556,13 @@ export async function search(request: Request, env: Env): Promise<Response> {
}
if (!type || type === "statuses") {
const viewer = await loadStatusViewer(request, env);
const remoteStatus = await resolveRemoteStatusSearch(env, q);
if (remoteStatus && await canViewerViewCachedStatus(env, remoteStatus, viewer)) {
statuses.push(await cachedStatusToMastodon(env, remoteStatus));
}
const rows = await env.DB.prepare("SELECT * FROM statuses WHERE content LIKE ? ORDER BY created_at DESC LIMIT 100").bind(`%${escapeHtml(q)}%`).all<Status>();
const visibleRows = await filterStatusesForViewer(env, rows.results, await loadStatusViewer(request, env));
const visibleRows = await filterStatusesForViewer(env, rows.results, viewer);
statuses.push(...await serializeStatuses(env, visibleRows.slice(0, 20), request));
}
@@ -1563,6 +1575,74 @@ export async function search(request: Request, env: Env): Promise<Response> {
return json({ accounts, statuses, hashtags });
}
async function resolveRemoteStatusSearch(env: Env, q: string): Promise<CachedStatus | null> {
const url = parseSearchUrl(q);
if (!url || url.host.toLowerCase() === hostFromBaseUrl(env).toLowerCase()) return null;
const cached = await env.DB.prepare("SELECT * FROM cached_statuses WHERE object_id = ? OR url = ? LIMIT 1")
.bind(url.toString(), url.toString()).first<CachedStatus>();
if (cached) return cached;
let data: Json;
try {
const response = await fetch(url.toString(), {
headers: { accept: ACTIVITY_JSON_ACCEPT },
signal: AbortSignal.timeout(10_000),
cf: { cacheTtl: 60 }
});
if (!response.ok) return null;
data = await response.json() as Json;
} catch {
return null;
}
const resolved = remoteNoteFromFetchedObject(data);
if (!resolved) return null;
await resolveRemoteActor(env, resolved.actorId);
const stored = await cacheRemoteNote(env, resolved.actorId, resolved.note, resolved.activity);
return stored ?? getCachedStatusByObjectId(env, String(resolved.note.id));
}
function parseSearchUrl(value: string): URL | null {
try {
const url = new URL(value);
return url.protocol === "https:" || url.protocol === "http:" ? url : null;
} catch {
return null;
}
}
function remoteNoteFromFetchedObject(value: Json): { actorId: string; note: Json; activity: Json } | null {
const type = String(value.type ?? "");
if (type === "Note") {
const actorId = actorIdFromField(value.attributedTo);
return actorId && typeof value.id === "string" ? { actorId, note: value, activity: {} } : null;
}
const note = objectAsJson(value.object);
if (type === "Create" && note && String(note.type ?? "") === "Note" && typeof note.id === "string") {
const actorId = actorIdFromField(value.actor) ?? actorIdFromField(note.attributedTo);
return actorId ? { actorId, note, activity: value } : null;
}
if (type === "Announce" && note && String(note.type ?? "") === "Note" && typeof note.id === "string") {
const actorId = actorIdFromField(note.attributedTo) ?? actorIdFromField(value.actor);
return actorId ? { actorId, note, activity: value } : null;
}
return null;
}
function actorIdFromField(value: unknown): string | null {
if (typeof value === "string" && value) return value;
if (Array.isArray(value)) {
for (const item of value) {
const actorId = actorIdFromField(item);
if (actorId) return actorId;
}
}
const obj = objectAsJson(value);
return typeof obj?.id === "string" && obj.id ? obj.id : null;
}
export async function customEmojis(env: Env): Promise<Response> {
void env;
return json([]);
@@ -1632,8 +1712,43 @@ export async function deletePushSubscription(request: Request, env: Env): Promis
}
export async function markersList(request: Request, env: Env): Promise<Response> {
void request; void env;
return json({});
const user = await requireUser(request, env);
const url = new URL(request.url);
const timelines = uniqueStrings(url.searchParams.getAll("timeline[]").concat(url.searchParams.getAll("timeline")));
if (timelines.length === 0) return json({});
const rows = await listMarkers(env, user.id, timelines);
return json(markersJson(rows));
}
export async function updateMarkers(request: Request, env: Env): Promise<Response> {
const user = await requireUser(request, env);
const body = await readBody(request);
const out: Record<string, unknown> = {};
for (const timeline of ["home", "notifications"]) {
const lastReadId = bodyString(body, `${timeline}[last_read_id]`).trim();
if (!lastReadId) continue;
const result = await saveMarker(env, user.id, timeline, lastReadId);
if (result.conflict) throw new HttpError(409, "Conflict during update, please try again");
if (result.marker) out[timeline] = markerJson(result.marker);
}
return json(out);
}
function markersJson(rows: Marker[]): Record<string, unknown> {
const out: Record<string, unknown> = {};
for (const row of rows) out[row.timeline] = markerJson(row);
return out;
}
function markerJson(row: Marker): Record<string, unknown> {
return {
last_read_id: row.last_read_id,
version: row.version,
updated_at: row.updated_at
};
}
function pushSubscriptionJson(row: PushSubscription): Record<string, unknown> {