@fedify/fedify
Version:
An ActivityPub server framework
131 lines (130 loc) • 4.55 kB
JavaScript
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}`);
}
}