diff --git a/src/activitypub.ts b/src/activitypub.ts index 31adbc1..81ed46e 100644 --- a/src/activitypub.ts +++ b/src/activitypub.ts @@ -8,6 +8,7 @@ import { getStatus, getStatusByObjectId, getUserByUsername, + listMediaForStatus, listProfileFields, recordNotification, upsertActorCache, @@ -33,7 +34,7 @@ import { PUBLIC_COLLECTION, SECURITY_CONTEXT } from "./types"; -import type { ActorCache, CachedStatus, CachedStatusMention, CachedStatusTag, Json, RemoteActor, Status, User } from "./types"; +import type { ActorCache, CachedStatus, CachedStatusMention, CachedStatusTag, Json, Media, RemoteActor, Status, User } from "./types"; import { actorUrl, activityUrl, @@ -175,7 +176,7 @@ export async function outbox(request: Request, env: Env, username: string): Prom const rows = await env.DB.prepare( "SELECT * FROM statuses WHERE user_id = ? AND visibility IN ('public', 'unlisted') ORDER BY created_at DESC LIMIT ?" ).bind(user.id, limit).all(); - const items = rows.results.map((status) => createActivity(env, user, status)); + const items = await Promise.all(rows.results.map((status) => createActivity(env, user, status))); return activityJson({ "@context": ACTIVITY_CONTEXT, id: `${base}?page=true`, @@ -216,7 +217,8 @@ export async function activityObject(env: Env, objectId: string): Promise(); if (!user) return json({ error: "not_found" }, 404); - return activityJson(noteObject(env, user, status)); + const attachments = await loadStatusAttachments(env, status.id); + return activityJson(noteObject(env, user, status, { attachments })); } const tomb = await env.DB.prepare("SELECT * FROM deleted_statuses WHERE id = ?").bind(objectId).first<{ id: string; deleted_at: string }>(); if (tomb) { @@ -596,10 +598,11 @@ async function localUserFromTarget(env: Env, actorId: string | null): Promise { const audience = statusAudience(env, user, status); const to = extra.to ?? audience.to; const cc = extra.cc ?? audience.cc; + const attachments = await loadStatusAttachments(env, status.id); return { "@context": [ACTIVITY_CONTEXT, SECURITY_CONTEXT], id: status.activity_id, @@ -608,10 +611,24 @@ export function createActivity(env: Env, user: User, status: Status, extra: { to published: status.created_at, to, cc, - object: noteObject(env, user, status, { to, cc }) + object: noteObject(env, user, status, { to, cc, attachments }) }; } +export function attachmentObject(env: Env, media: Media): Json { + return { + type: media.mime_type.startsWith("image/") ? "Image" : "Document", + mediaType: media.mime_type, + url: mediaUrl(env, media.r2_key), + name: media.description ?? null + }; +} + +export async function loadStatusAttachments(env: Env, statusId: string): Promise { + const media = await listMediaForStatus(env, statusId); + return media.map((item) => attachmentObject(env, item)); +} + function statusAudience(env: Env, user: User, status: Status): { to: string[]; cc: string[] } { if (status.visibility === "unlisted") { return { to: [`${actorUrl(env, user)}/followers`], cc: [PUBLIC_COLLECTION] }; diff --git a/src/mastodon.ts b/src/mastodon.ts index 6017c50..e73b0e0 100644 --- a/src/mastodon.ts +++ b/src/mastodon.ts @@ -696,7 +696,7 @@ async function publishStatus(env: Env, user: User, input: StatusCreateInput): Pr : input.visibility === "unlisted" ? ["https://www.w3.org/ns/activitystreams#Public", ...mentionActors] : []; - const activity = createActivity(env, user, status, { to, cc }); + const activity = await createActivity(env, user, status, { to, cc }); await deliverToInboxes(env, user, inboxes, activity); } else if (input.visibility === "direct") { const inboxes = new Set(); @@ -706,7 +706,7 @@ async function publishStatus(env: Env, user: User, input: StatusCreateInput): Pr if (cache) inboxes.add(cache.shared_inbox ?? cache.inbox); } } - const activity = createActivity(env, user, status, { to: resolvedMentions.map((m) => m.actorId), cc: [] }); + const activity = await createActivity(env, user, status, { to: resolvedMentions.map((m) => m.actorId), cc: [] }); await deliverToInboxes(env, user, inboxes, activity); }