@fedify/fedify
Version:
An ActivityPub server framework
147 lines (146 loc) • 5.79 kB
JavaScript
import * as dntShim from "../_dnt.shims.js";
import { getLogger } from "@logtape/logtape";
import { context, propagation, SpanKind, SpanStatusCode, trace, } from "@opentelemetry/api";
import metadata from "../deno.js";
import { getTypeId } from "../vocab/type.js";
import { Activity } from "../vocab/vocab.js";
export class InboxListenerSet {
#listeners;
constructor() {
this.#listeners = new Map();
}
add(
// deno-lint-ignore no-explicit-any
type, listener) {
if (this.#listeners.has(type)) {
throw new TypeError("Listener already set for this type.");
}
this.#listeners.set(type, listener);
}
dispatchWithClass(activity) {
// deno-lint-ignore no-explicit-any
let cls = activity
// deno-lint-ignore no-explicit-any
.constructor;
const inboxListeners = this.#listeners;
if (inboxListeners == null) {
return null;
}
while (true) {
if (inboxListeners.has(cls))
break;
if (cls === Activity)
return null;
cls = globalThis.Object.getPrototypeOf(cls);
}
const listener = inboxListeners.get(cls);
return { class: cls, listener };
}
dispatch(activity) {
return this.dispatchWithClass(activity)?.listener ?? null;
}
}
export async function routeActivity({ context: ctx, json, activity, recipient, inboxListeners, inboxContextFactory, inboxErrorHandler, kv, kvPrefixes, queue, span, tracerProvider, }) {
const logger = getLogger(["fedify", "federation", "inbox"]);
const cacheKey = activity.id == null ? null : [
...kvPrefixes.activityIdempotence,
ctx.origin,
activity.id.href,
];
if (cacheKey != null) {
const cached = await kv.get(cacheKey);
if (cached === true) {
logger.debug("Activity {activityId} has already been processed.", {
activityId: activity.id?.href,
activity: json,
recipient,
});
span.setStatus({
code: SpanStatusCode.UNSET,
message: `Activity ${activity.id?.href} has already been processed.`,
});
return "alreadyProcessed";
}
}
if (activity.actorId == null) {
logger.error("Missing actor.", { activity: json });
span.setStatus({ code: SpanStatusCode.ERROR, message: "Missing actor." });
return "missingActor";
}
span.setAttribute("activitypub.actor.id", activity.actorId.href);
if (queue != null) {
const carrier = {};
propagation.inject(context.active(), carrier);
try {
await queue.enqueue({
type: "inbox",
id: dntShim.crypto.randomUUID(),
baseUrl: ctx.origin,
activity: json,
identifier: recipient,
attempt: 0,
started: new Date().toISOString(),
traceContext: carrier,
});
}
catch (error) {
logger.error("Failed to enqueue the incoming activity {activityId}:\n{error}", { error, activityId: activity.id?.href, activity: json, recipient });
span.setStatus({
code: SpanStatusCode.ERROR,
message: `Failed to enqueue the incoming activity ${activity.id?.href}.`,
});
throw error;
}
logger.info("Activity {activityId} is enqueued.", { activityId: activity.id?.href, activity: json, recipient });
return "enqueued";
}
tracerProvider = tracerProvider ?? trace.getTracerProvider();
const tracer = tracerProvider.getTracer(metadata.name, metadata.version);
return await tracer.startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (span) => {
const dispatched = inboxListeners?.dispatchWithClass(activity);
if (dispatched == null) {
logger.error("Unsupported activity type:\n{activity}", { activity: json, recipient });
span.setStatus({
code: SpanStatusCode.UNSET,
message: `Unsupported activity type: ${getTypeId(activity).href}`,
});
span.end();
return "unsupportedActivity";
}
const { class: cls, listener } = dispatched;
span.updateName(`activitypub.dispatch_inbox_listener ${cls.name}`);
try {
await listener(inboxContextFactory(recipient, json, activity?.id?.href, getTypeId(activity).href), activity);
}
catch (error) {
try {
await inboxErrorHandler?.(ctx, error);
}
catch (error) {
logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
error,
activityId: activity.id?.href,
activity: json,
recipient,
});
}
logger.error("Failed to process the incoming activity {activityId}:\n{error}", {
error,
activityId: activity.id?.href,
activity: json,
recipient,
});
span.setStatus({ code: SpanStatusCode.ERROR, message: String(error) });
span.end();
return "error";
}
if (cacheKey != null) {
await kv.set(cacheKey, true, {
ttl: dntShim.Temporal.Duration.from({ days: 1 }),
});
}
logger.info("Activity {activityId} has been processed.", { activityId: activity.id?.href, activity: json, recipient });
span.end();
return "success";
});
}