UNPKG

@fedify/fedify

Version:

An ActivityPub server framework

147 lines (146 loc) 5.79 kB
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"; }); }