@fedify/fedify
Version:
An ActivityPub server framework
171 lines (167 loc) • 5.07 kB
JavaScript
import { Temporal } from "@js-temporal/polyfill";
import { URLPattern } from "urlpattern-polyfill";
globalThis.addEventListener = () => {};
import { deno_default } from "./docloader-C8mmLN-Y.js";
import { Activity } from "./vocab-dDpPQ0fF.js";
import { getTypeId } from "./type-D2s5lmbZ.js";
import { getLogger } from "@logtape/logtape";
import { SpanKind, SpanStatusCode, context, propagation, trace } from "@opentelemetry/api";
//#region federation/inbox.ts
var InboxListenerSet = class InboxListenerSet {
#listeners;
constructor() {
this.#listeners = /* @__PURE__ */ new Map();
}
clone() {
const clone = new InboxListenerSet();
clone.#listeners = new Map(this.#listeners);
return clone;
}
add(type, listener) {
if (this.#listeners.has(type)) throw new TypeError("Listener already set for this type.");
this.#listeners.set(type, listener);
}
dispatchWithClass(activity) {
let cls = activity.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;
}
};
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: crypto.randomUUID(),
baseUrl: ctx.origin,
activity: json,
identifier: recipient,
attempt: 0,
started: (/* @__PURE__ */ 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(deno_default.name, deno_default.version);
return await tracer.startActiveSpan("activitypub.dispatch_inbox_listener", { kind: SpanKind.INTERNAL }, async (span$1) => {
const dispatched = inboxListeners?.dispatchWithClass(activity);
if (dispatched == null) {
logger.error("Unsupported activity type:\n{activity}", {
activity: json,
recipient
});
span$1.setStatus({
code: SpanStatusCode.UNSET,
message: `Unsupported activity type: ${getTypeId(activity).href}`
});
span$1.end();
return "unsupportedActivity";
}
const { class: cls, listener } = dispatched;
span$1.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$1) {
logger.error("An unexpected error occurred in inbox error handler:\n{error}", {
error: error$1,
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$1.setStatus({
code: SpanStatusCode.ERROR,
message: String(error)
});
span$1.end();
return "error";
}
if (cacheKey != null) await kv.set(cacheKey, true, { ttl: Temporal.Duration.from({ days: 1 }) });
logger.info("Activity {activityId} has been processed.", {
activityId: activity.id?.href,
activity: json,
recipient
});
span$1.end();
return "success";
});
}
//#endregion
export { InboxListenerSet, routeActivity };