add
This commit is contained in:
+118
-3
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user