UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

131 lines (130 loc) 4.55 kB
import { getLogger } from "@logtape/logtape"; import { SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api"; import metadata from "../deno.js"; import { signRequest } from "../sig/http.js"; /** * Extracts the inbox URLs from recipients. * @param parameters The parameters to extract the inboxes. * See also {@link ExtractInboxesParameters}. * @returns The inboxes as a map of inbox URL to actor URIs. */ export function extractInboxes({ recipients, preferSharedInbox, excludeBaseUris }) { const inboxes = {}; for (const recipient of recipients) { let inbox; let sharedInbox = false; if (preferSharedInbox && recipient.endpoints?.sharedInbox != null) { inbox = recipient.endpoints.sharedInbox; sharedInbox = true; } else { inbox = recipient.inboxId; } if (inbox != null && recipient.id != null) { if (excludeBaseUris != null && excludeBaseUris.some((u) => u.origin === inbox?.origin)) { continue; } inboxes[inbox.href] ??= { actorIds: new Set(), sharedInbox }; inboxes[inbox.href].actorIds.add(recipient.id.href); } } return inboxes; } /** * Sends an {@link Activity} to an inbox. * * @param parameters The parameters for sending the activity. * See also {@link SendActivityParameters}. * @throws {Error} If the activity fails to send. */ export function sendActivity(options) { const tracerProvider = options.tracerProvider ?? trace.getTracerProvider(); const tracer = tracerProvider.getTracer(metadata.name, metadata.version); return tracer.startActiveSpan("activitypub.send_activity", { kind: SpanKind.CLIENT, attributes: { "activitypub.shared_inbox": options.sharedInbox ?? false, }, }, async (span) => { if (options.activityId != null) { span.setAttribute("activitypub.activity.id", options.activityId); } if (options.activityType != null) { span.setAttribute("activitypub.activity.type", options.activityType); } try { await sendActivityInternal({ ...options, tracerProvider }); } catch (e) { span.setStatus({ code: SpanStatusCode.ERROR, message: String(e) }); throw e; } finally { span.end(); } }); } async function sendActivityInternal({ activity, activityId, keys, inbox, headers, tracerProvider, }) { const logger = getLogger(["fedify", "federation", "outbox"]); headers = new Headers(headers); headers.set("Content-Type", "application/activity+json"); let request = new Request(inbox, { method: "POST", headers, body: JSON.stringify(activity), }); let rsaKey = null; for (const key of keys) { if (key.privateKey.algorithm.name === "RSASSA-PKCS1-v1_5") { rsaKey = key; break; } } if (rsaKey == null) { logger.warn("No supported key found to sign the request to {inbox}. " + "The request will be sent without a signature. " + "In order to sign the request, at least one RSASSA-PKCS1-v1_5 key " + "must be provided.", { inbox: inbox.href, keys: keys.map((pair) => ({ keyId: pair.keyId.href, privateKey: pair.privateKey, })), }); } else { request = await signRequest(request, rsaKey.privateKey, rsaKey.keyId, { tracerProvider }); } let response; try { response = await fetch(request); } catch (error) { logger.error("Failed to send activity {activityId} to {inbox}:\n{error}", { activityId, inbox: inbox.href, error, }); throw error; } if (!response.ok) { let error; try { error = await response.text(); } catch (_) { error = ""; } logger.error("Failed to send activity {activityId} to {inbox} ({status} " + "{statusText}):\n{error}", { activityId, inbox: inbox.href, status: response.status, statusText: response.statusText, error, }); throw new Error(`Failed to send activity ${activityId} to ${inbox.href} ` + `(${response.status} ${response.statusText}):\n${error}`); } }